1 /* 2 * Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package java.util; 27 28 import java.io.BufferedInputStream; 29 import java.io.DataInputStream; 30 import java.io.File; 31 import java.io.FileReader; 32 import java.io.InputStream; 33 import java.io.IOException; 34 import java.io.Serializable; 35 import java.security.AccessController; 36 import java.security.PrivilegedAction; 37 import java.text.ParseException; 38 import java.text.SimpleDateFormat; 39 import java.util.concurrent.ConcurrentHashMap; 40 import java.util.concurrent.ConcurrentMap; 41 import java.util.regex.Pattern; 42 import java.util.regex.Matcher; 43 import java.util.spi.CurrencyNameProvider; 44 import java.util.stream.Collectors; 45 46 import jdk.internal.util.StaticProperty; 47 import sun.util.locale.provider.CalendarDataUtility; 48 import sun.util.locale.provider.LocaleServiceProviderPool; 49 import sun.util.logging.PlatformLogger; 50 51 52 /** 53 * Represents a currency. Currencies are identified by their ISO 4217 currency 54 * codes. Visit the <a href="http://www.iso.org/iso/home/standards/currency_codes.htm"> 55 * ISO web site</a> for more information. 56 * <p> 57 * The class is designed so that there's never more than one 58 * <code>Currency</code> instance for any given currency. Therefore, there's 59 * no public constructor. You obtain a <code>Currency</code> instance using 60 * the <code>getInstance</code> methods. 61 * <p> 62 * Users can supersede the Java runtime currency data by means of the system 63 * property {@systemProperty java.util.currency.data}. If this system property is 64 * defined then its value is the location of a properties file, the contents of 65 * which are key/value pairs of the ISO 3166 country codes and the ISO 4217 66 * currency data respectively. The value part consists of three ISO 4217 values 67 * of a currency, i.e., an alphabetic code, a numeric code, and a minor unit. 68 * Those three ISO 4217 values are separated by commas. 69 * The lines which start with '#'s are considered comment lines. An optional UTC 70 * timestamp may be specified per currency entry if users need to specify a 71 * cutover date indicating when the new data comes into effect. The timestamp is 72 * appended to the end of the currency properties and uses a comma as a separator. 73 * If a UTC datestamp is present and valid, the JRE will only use the new currency 74 * properties if the current UTC date is later than the date specified at class 75 * loading time. The format of the timestamp must be of ISO 8601 format : 76 * {@code 'yyyy-MM-dd'T'HH:mm:ss'}. For example, 77 * <p> 78 * <code> 79 * #Sample currency properties<br> 80 * JP=JPZ,999,0 81 * </code> 82 * <p> 83 * will supersede the currency data for Japan. If JPZ is one of the existing 84 * ISO 4217 currency code referred by other countries, the existing 85 * JPZ currency data is updated with the given numeric code and minor 86 * unit value. 87 * 88 * <p> 89 * <code> 90 * #Sample currency properties with cutover date<br> 91 * JP=JPZ,999,0,2014-01-01T00:00:00 92 * </code> 93 * <p> 94 * will supersede the currency data for Japan if {@code Currency} class is loaded after 95 * 1st January 2014 00:00:00 GMT. 96 * <p> 97 * Where syntactically malformed entries are encountered, the entry is ignored 98 * and the remainder of entries in file are processed. For instances where duplicate 99 * country code entries exist, the behavior of the Currency information for that 100 * {@code Currency} is undefined and the remainder of entries in file are processed. 101 * <p> 102 * If multiple property entries with same currency code but different numeric code 103 * and/or minor unit are encountered, those entries are ignored and the remainder 104 * of entries in file are processed. 105 * 106 * <p> 107 * It is recommended to use {@link java.math.BigDecimal} class while dealing 108 * with {@code Currency} or monetary values as it provides better handling of floating 109 * point numbers and their operations. 110 * 111 * @see java.math.BigDecimal 112 * @since 1.4 113 */ 114 public final class Currency implements Serializable { 115 116 private static final long serialVersionUID = -158308464356906721L; 117 118 /** 119 * ISO 4217 currency code for this currency. 120 * 121 * @serial 122 */ 123 private final String currencyCode; 124 125 /** 126 * Default fraction digits for this currency. 127 * Set from currency data tables. 128 */ 129 private final transient int defaultFractionDigits; 130 131 /** 132 * ISO 4217 numeric code for this currency. 133 * Set from currency data tables. 134 */ 135 private final transient int numericCode; 136 137 138 // class data: instance map 139 140 private static ConcurrentMap<String, Currency> instances = new ConcurrentHashMap<>(7); 141 private static HashSet<Currency> available; 142 143 // Class data: currency data obtained from currency.data file. 144 // Purpose: 145 // - determine valid country codes 146 // - determine valid currency codes 147 // - map country codes to currency codes 148 // - obtain default fraction digits for currency codes 149 // 150 // sc = special case; dfd = default fraction digits 151 // Simple countries are those where the country code is a prefix of the 152 // currency code, and there are no known plans to change the currency. 153 // 154 // table formats: 155 // - mainTable: 156 // - maps country code to 32-bit int 157 // - 26*26 entries, corresponding to [A-Z]*[A-Z] 158 // - \u007F -> not valid country 159 // - bits 20-31: unused 160 // - bits 10-19: numeric code (0 to 1023) 161 // - bit 9: 1 - special case, bits 0-4 indicate which one 162 // 0 - simple country, bits 0-4 indicate final char of currency code 163 // - bits 5-8: fraction digits for simple countries, 0 for special cases 164 // - bits 0-4: final char for currency code for simple country, or ID of special case 165 // - special case IDs: 166 // - 0: country has no currency 167 // - other: index into specialCasesList 168 169 static int formatVersion; 170 static int dataVersion; 171 static int[] mainTable; 172 static List<SpecialCaseEntry> specialCasesList; 173 static List<OtherCurrencyEntry> otherCurrenciesList; 174 175 // handy constants - must match definitions in GenerateCurrencyData 176 // magic number 177 private static final int MAGIC_NUMBER = 0x43757244; 178 // number of characters from A to Z 179 private static final int A_TO_Z = ('Z' - 'A') + 1; 180 // entry for invalid country codes 181 private static final int INVALID_COUNTRY_ENTRY = 0x0000007F; 182 // entry for countries without currency 183 private static final int COUNTRY_WITHOUT_CURRENCY_ENTRY = 0x00000200; 184 // mask for simple case country entries 185 private static final int SIMPLE_CASE_COUNTRY_MASK = 0x00000000; 186 // mask for simple case country entry final character 187 private static final int SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK = 0x0000001F; 188 // mask for simple case country entry default currency digits 189 private static final int SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK = 0x000001E0; 190 // shift count for simple case country entry default currency digits 191 private static final int SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT = 5; 192 // maximum number for simple case country entry default currency digits 193 private static final int SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS = 9; 194 // mask for special case country entries 195 private static final int SPECIAL_CASE_COUNTRY_MASK = 0x00000200; 196 // mask for special case country index 197 private static final int SPECIAL_CASE_COUNTRY_INDEX_MASK = 0x0000001F; 198 // delta from entry index component in main table to index into special case tables 199 private static final int SPECIAL_CASE_COUNTRY_INDEX_DELTA = 1; 200 // mask for distinguishing simple and special case countries 201 private static final int COUNTRY_TYPE_MASK = SIMPLE_CASE_COUNTRY_MASK | SPECIAL_CASE_COUNTRY_MASK; 202 // mask for the numeric code of the currency 203 private static final int NUMERIC_CODE_MASK = 0x000FFC00; 204 // shift count for the numeric code of the currency 205 private static final int NUMERIC_CODE_SHIFT = 10; 206 207 // Currency data format version 208 private static final int VALID_FORMAT_VERSION = 3; 209 210 static { 211 AccessController.doPrivileged(new PrivilegedAction<>() { 212 @Override 213 public Void run() { 214 try { 215 try (InputStream in = getClass().getResourceAsStream("/java/util/currency.data")) { 216 if (in == null) { 217 throw new InternalError("Currency data not found"); 218 } 219 DataInputStream dis = new DataInputStream(new BufferedInputStream(in)); 220 if (dis.readInt() != MAGIC_NUMBER) { 221 throw new InternalError("Currency data is possibly corrupted"); 222 } 223 formatVersion = dis.readInt(); 224 if (formatVersion != VALID_FORMAT_VERSION) { 225 throw new InternalError("Currency data format is incorrect"); 226 } 227 dataVersion = dis.readInt(); 228 mainTable = readIntArray(dis, A_TO_Z * A_TO_Z); 229 int scCount = dis.readInt(); 230 specialCasesList = readSpecialCases(dis, scCount); 231 int ocCount = dis.readInt(); 232 otherCurrenciesList = readOtherCurrencies(dis, ocCount); 233 } 234 } catch (IOException e) { 235 throw new InternalError(e); 236 } 237 238 // look for the properties file for overrides 239 String propsFile = System.getProperty("java.util.currency.data"); 240 if (propsFile == null) { 241 propsFile = StaticProperty.javaHome() + File.separator + "lib" + 242 File.separator + "currency.properties"; 243 } 244 try { 245 File propFile = new File(propsFile); 246 if (propFile.exists()) { 247 Properties props = new Properties(); 248 try (FileReader fr = new FileReader(propFile)) { 249 props.load(fr); 250 } 251 Pattern propertiesPattern = 252 Pattern.compile("([A-Z]{3})\\s*,\\s*(\\d{3})\\s*,\\s*" + 253 "(\\d+)\\s*,?\\s*(\\d{4}-\\d{2}-\\d{2}T\\d{2}:" + 254 "\\d{2}:\\d{2})?"); 255 List<CurrencyProperty> currencyEntries 256 = getValidCurrencyData(props, propertiesPattern); 257 currencyEntries.forEach(Currency::replaceCurrencyData); 258 } 259 } catch (IOException e) { 260 CurrencyProperty.info("currency.properties is ignored" 261 + " because of an IOException", e); 262 } 263 return null; 264 } 265 }); 266 } 267 268 /** 269 * Constants for retrieving localized names from the name providers. 270 */ 271 private static final int SYMBOL = 0; 272 private static final int DISPLAYNAME = 1; 273 274 275 /** 276 * Constructs a <code>Currency</code> instance. The constructor is private 277 * so that we can insure that there's never more than one instance for a 278 * given currency. 279 */ 280 private Currency(String currencyCode, int defaultFractionDigits, int numericCode) { 281 this.currencyCode = currencyCode; 282 this.defaultFractionDigits = defaultFractionDigits; 283 this.numericCode = numericCode; 284 } 285 286 /** 287 * Returns the <code>Currency</code> instance for the given currency code. 288 * 289 * @param currencyCode the ISO 4217 code of the currency 290 * @return the <code>Currency</code> instance for the given currency code 291 * @exception NullPointerException if <code>currencyCode</code> is null 292 * @exception IllegalArgumentException if <code>currencyCode</code> is not 293 * a supported ISO 4217 code. 294 */ 295 public static Currency getInstance(String currencyCode) { 296 return getInstance(currencyCode, Integer.MIN_VALUE, 0); 297 } 298 299 private static Currency getInstance(String currencyCode, int defaultFractionDigits, 300 int numericCode) { 301 // Try to look up the currency code in the instances table. 302 // This does the null pointer check as a side effect. 303 // Also, if there already is an entry, the currencyCode must be valid. 304 Currency instance = instances.get(currencyCode); 305 if (instance != null) { 306 return instance; 307 } 308 309 if (defaultFractionDigits == Integer.MIN_VALUE) { 310 // Currency code not internally generated, need to verify first 311 // A currency code must have 3 characters and exist in the main table 312 // or in the list of other currencies. 313 boolean found = false; 314 if (currencyCode.length() != 3) { 315 throw new IllegalArgumentException(); 316 } 317 char char1 = currencyCode.charAt(0); 318 char char2 = currencyCode.charAt(1); 319 int tableEntry = getMainTableEntry(char1, char2); 320 if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK 321 && tableEntry != INVALID_COUNTRY_ENTRY 322 && currencyCode.charAt(2) - 'A' == (tableEntry & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK)) { 323 defaultFractionDigits = (tableEntry & SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK) >> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT; 324 numericCode = (tableEntry & NUMERIC_CODE_MASK) >> NUMERIC_CODE_SHIFT; 325 found = true; 326 } else { //special case 327 int[] fractionAndNumericCode = SpecialCaseEntry.findEntry(currencyCode); 328 if (fractionAndNumericCode != null) { 329 defaultFractionDigits = fractionAndNumericCode[0]; 330 numericCode = fractionAndNumericCode[1]; 331 found = true; 332 } 333 } 334 335 if (!found) { 336 OtherCurrencyEntry ocEntry = OtherCurrencyEntry.findEntry(currencyCode); 337 if (ocEntry == null) { 338 throw new IllegalArgumentException(); 339 } 340 defaultFractionDigits = ocEntry.fraction; 341 numericCode = ocEntry.numericCode; 342 } 343 } 344 345 Currency currencyVal = 346 new Currency(currencyCode, defaultFractionDigits, numericCode); 347 instance = instances.putIfAbsent(currencyCode, currencyVal); 348 return (instance != null ? instance : currencyVal); 349 } 350 351 /** 352 * Returns the <code>Currency</code> instance for the country of the 353 * given locale. The language and variant components of the locale 354 * are ignored. The result may vary over time, as countries change their 355 * currencies. For example, for the original member countries of the 356 * European Monetary Union, the method returns the old national currencies 357 * until December 31, 2001, and the Euro from January 1, 2002, local time 358 * of the respective countries. 359 * <p> 360 * If the specified {@code locale} contains "cu" and/or "rg" 361 * <a href="./Locale.html#def_locale_extension">Unicode extensions</a>, 362 * the instance returned from this method reflects 363 * the values specified with those extensions. If both "cu" and "rg" are 364 * specified, the currency from the "cu" extension supersedes the implicit one 365 * from the "rg" extension. 366 * <p> 367 * The method returns <code>null</code> for territories that don't 368 * have a currency, such as Antarctica. 369 * 370 * @param locale the locale for whose country a <code>Currency</code> 371 * instance is needed 372 * @return the <code>Currency</code> instance for the country of the given 373 * locale, or {@code null} 374 * @exception NullPointerException if <code>locale</code> 375 * is {@code null} 376 * @exception IllegalArgumentException if the country of the given {@code locale} 377 * is not a supported ISO 3166 country code. 378 */ 379 public static Currency getInstance(Locale locale) { 380 // check for locale overrides 381 String override = locale.getUnicodeLocaleType("cu"); 382 if (override != null) { 383 try { 384 return getInstance(override.toUpperCase(Locale.ROOT)); 385 } catch (IllegalArgumentException iae) { 386 // override currency is invalid. Fall through. 387 } 388 } 389 390 String country = CalendarDataUtility.findRegionOverride(locale).getCountry(); 391 392 if (country == null || !country.matches("^[a-zA-Z]{2}$")) { 393 throw new IllegalArgumentException(); 394 } 395 396 char char1 = country.charAt(0); 397 char char2 = country.charAt(1); 398 int tableEntry = getMainTableEntry(char1, char2); 399 if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK 400 && tableEntry != INVALID_COUNTRY_ENTRY) { 401 char finalChar = (char) ((tableEntry & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK) + 'A'); 402 int defaultFractionDigits = (tableEntry & SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK) >> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT; 403 int numericCode = (tableEntry & NUMERIC_CODE_MASK) >> NUMERIC_CODE_SHIFT; 404 StringBuilder sb = new StringBuilder(country); 405 sb.append(finalChar); 406 return getInstance(sb.toString(), defaultFractionDigits, numericCode); 407 } else { 408 // special cases 409 if (tableEntry == INVALID_COUNTRY_ENTRY) { 410 throw new IllegalArgumentException(); 411 } 412 if (tableEntry == COUNTRY_WITHOUT_CURRENCY_ENTRY) { 413 return null; 414 } else { 415 int index = SpecialCaseEntry.toIndex(tableEntry); 416 SpecialCaseEntry scEntry = specialCasesList.get(index); 417 if (scEntry.cutOverTime == Long.MAX_VALUE 418 || System.currentTimeMillis() < scEntry.cutOverTime) { 419 return getInstance(scEntry.oldCurrency, 420 scEntry.oldCurrencyFraction, 421 scEntry.oldCurrencyNumericCode); 422 } else { 423 return getInstance(scEntry.newCurrency, 424 scEntry.newCurrencyFraction, 425 scEntry.newCurrencyNumericCode); 426 } 427 } 428 } 429 } 430 431 /** 432 * Gets the set of available currencies. The returned set of currencies 433 * contains all of the available currencies, which may include currencies 434 * that represent obsolete ISO 4217 codes. The set can be modified 435 * without affecting the available currencies in the runtime. 436 * 437 * @return the set of available currencies. If there is no currency 438 * available in the runtime, the returned set is empty. 439 * @since 1.7 440 */ 441 public static Set<Currency> getAvailableCurrencies() { 442 synchronized(Currency.class) { 443 if (available == null) { 444 available = new HashSet<>(256); 445 446 // Add simple currencies first 447 for (char c1 = 'A'; c1 <= 'Z'; c1 ++) { 448 for (char c2 = 'A'; c2 <= 'Z'; c2 ++) { 449 int tableEntry = getMainTableEntry(c1, c2); 450 if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK 451 && tableEntry != INVALID_COUNTRY_ENTRY) { 452 char finalChar = (char) ((tableEntry & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK) + 'A'); 453 int defaultFractionDigits = (tableEntry & SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK) >> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT; 454 int numericCode = (tableEntry & NUMERIC_CODE_MASK) >> NUMERIC_CODE_SHIFT; 455 StringBuilder sb = new StringBuilder(); 456 sb.append(c1); 457 sb.append(c2); 458 sb.append(finalChar); 459 available.add(getInstance(sb.toString(), defaultFractionDigits, numericCode)); 460 } else if ((tableEntry & COUNTRY_TYPE_MASK) == SPECIAL_CASE_COUNTRY_MASK 461 && tableEntry != INVALID_COUNTRY_ENTRY 462 && tableEntry != COUNTRY_WITHOUT_CURRENCY_ENTRY) { 463 int index = SpecialCaseEntry.toIndex(tableEntry); 464 SpecialCaseEntry scEntry = specialCasesList.get(index); 465 466 if (scEntry.cutOverTime == Long.MAX_VALUE 467 || System.currentTimeMillis() < scEntry.cutOverTime) { 468 available.add(getInstance(scEntry.oldCurrency, 469 scEntry.oldCurrencyFraction, 470 scEntry.oldCurrencyNumericCode)); 471 } else { 472 available.add(getInstance(scEntry.newCurrency, 473 scEntry.newCurrencyFraction, 474 scEntry.newCurrencyNumericCode)); 475 } 476 } 477 } 478 } 479 480 // Now add other currencies 481 for (OtherCurrencyEntry entry : otherCurrenciesList) { 482 available.add(getInstance(entry.currencyCode)); 483 } 484 } 485 } 486 487 @SuppressWarnings("unchecked") 488 Set<Currency> result = (Set<Currency>) available.clone(); 489 return result; 490 } 491 492 /** 493 * Gets the ISO 4217 currency code of this currency. 494 * 495 * @return the ISO 4217 currency code of this currency. 496 */ 497 public String getCurrencyCode() { 498 return currencyCode; 499 } 500 501 /** 502 * Gets the symbol of this currency for the default 503 * {@link Locale.Category#DISPLAY DISPLAY} locale. 504 * For example, for the US Dollar, the symbol is "$" if the default 505 * locale is the US, while for other locales it may be "US$". If no 506 * symbol can be determined, the ISO 4217 currency code is returned. 507 * <p> 508 * If the default {@link Locale.Category#DISPLAY DISPLAY} locale 509 * contains "rg" (region override) 510 * <a href="./Locale.html#def_locale_extension">Unicode extension</a>, 511 * the symbol returned from this method reflects 512 * the value specified with that extension. 513 * <p> 514 * This is equivalent to calling 515 * {@link #getSymbol(Locale) 516 * getSymbol(Locale.getDefault(Locale.Category.DISPLAY))}. 517 * 518 * @return the symbol of this currency for the default 519 * {@link Locale.Category#DISPLAY DISPLAY} locale 520 */ 521 public String getSymbol() { 522 return getSymbol(Locale.getDefault(Locale.Category.DISPLAY)); 523 } 524 525 /** 526 * Gets the symbol of this currency for the specified locale. 527 * For example, for the US Dollar, the symbol is "$" if the specified 528 * locale is the US, while for other locales it may be "US$". If no 529 * symbol can be determined, the ISO 4217 currency code is returned. 530 * <p> 531 * If the specified {@code locale} contains "rg" (region override) 532 * <a href="./Locale.html#def_locale_extension">Unicode extension</a>, 533 * the symbol returned from this method reflects 534 * the value specified with that extension. 535 * 536 * @param locale the locale for which a display name for this currency is 537 * needed 538 * @return the symbol of this currency for the specified locale 539 * @exception NullPointerException if <code>locale</code> is null 540 */ 541 public String getSymbol(Locale locale) { 542 LocaleServiceProviderPool pool = 543 LocaleServiceProviderPool.getPool(CurrencyNameProvider.class); 544 locale = CalendarDataUtility.findRegionOverride(locale); 545 String symbol = pool.getLocalizedObject( 546 CurrencyNameGetter.INSTANCE, 547 locale, currencyCode, SYMBOL); 548 if (symbol != null) { 549 return symbol; 550 } 551 552 // use currency code as symbol of last resort 553 return currencyCode; 554 } 555 556 /** 557 * Gets the default number of fraction digits used with this currency. 558 * Note that the number of fraction digits is the same as ISO 4217's 559 * minor unit for the currency. 560 * For example, the default number of fraction digits for the Euro is 2, 561 * while for the Japanese Yen it's 0. 562 * In the case of pseudo-currencies, such as IMF Special Drawing Rights, 563 * -1 is returned. 564 * 565 * @return the default number of fraction digits used with this currency 566 */ 567 public int getDefaultFractionDigits() { 568 return defaultFractionDigits; 569 } 570 571 /** 572 * Returns the ISO 4217 numeric code of this currency. 573 * 574 * @return the ISO 4217 numeric code of this currency 575 * @since 1.7 576 */ 577 public int getNumericCode() { 578 return numericCode; 579 } 580 581 /** 582 * Returns the 3 digit ISO 4217 numeric code of this currency as a {@code String}. 583 * Unlike {@link #getNumericCode()}, which returns the numeric code as {@code int}, 584 * this method always returns the numeric code as a 3 digit string. 585 * e.g. a numeric value of 32 would be returned as "032", 586 * and a numeric value of 6 would be returned as "006". 587 * 588 * @return the 3 digit ISO 4217 numeric code of this currency as a {@code String} 589 * @since 9 590 */ 591 public String getNumericCodeAsString() { 592 /* numeric code could be returned as a 3 digit string simply by using 593 String.format("%03d",numericCode); which uses regex to parse the format, 594 "%03d" in this case. Parsing a regex gives an extra performance overhead, 595 so String.format() approach is avoided in this scenario. 596 */ 597 if (numericCode < 100) { 598 StringBuilder sb = new StringBuilder(); 599 sb.append('0'); 600 if (numericCode < 10) { 601 sb.append('0'); 602 } 603 return sb.append(numericCode).toString(); 604 } 605 return String.valueOf(numericCode); 606 } 607 608 /** 609 * Gets the name that is suitable for displaying this currency for 610 * the default {@link Locale.Category#DISPLAY DISPLAY} locale. 611 * If there is no suitable display name found 612 * for the default locale, the ISO 4217 currency code is returned. 613 * <p> 614 * This is equivalent to calling 615 * {@link #getDisplayName(Locale) 616 * getDisplayName(Locale.getDefault(Locale.Category.DISPLAY))}. 617 * 618 * @return the display name of this currency for the default 619 * {@link Locale.Category#DISPLAY DISPLAY} locale 620 * @since 1.7 621 */ 622 public String getDisplayName() { 623 return getDisplayName(Locale.getDefault(Locale.Category.DISPLAY)); 624 } 625 626 /** 627 * Gets the name that is suitable for displaying this currency for 628 * the specified locale. If there is no suitable display name found 629 * for the specified locale, the ISO 4217 currency code is returned. 630 * 631 * @param locale the locale for which a display name for this currency is 632 * needed 633 * @return the display name of this currency for the specified locale 634 * @exception NullPointerException if <code>locale</code> is null 635 * @since 1.7 636 */ 637 public String getDisplayName(Locale locale) { 638 LocaleServiceProviderPool pool = 639 LocaleServiceProviderPool.getPool(CurrencyNameProvider.class); 640 String result = pool.getLocalizedObject( 641 CurrencyNameGetter.INSTANCE, 642 locale, currencyCode, DISPLAYNAME); 643 if (result != null) { 644 return result; 645 } 646 647 // use currency code as symbol of last resort 648 return currencyCode; 649 } 650 651 /** 652 * Returns the ISO 4217 currency code of this currency. 653 * 654 * @return the ISO 4217 currency code of this currency 655 */ 656 @Override 657 public String toString() { 658 return currencyCode; 659 } 660 661 /** 662 * Resolves instances being deserialized to a single instance per currency. 663 */ 664 private Object readResolve() { 665 return getInstance(currencyCode); 666 } 667 668 /** 669 * Gets the main table entry for the country whose country code consists 670 * of char1 and char2. 671 */ 672 private static int getMainTableEntry(char char1, char char2) { 673 if (char1 < 'A' || char1 > 'Z' || char2 < 'A' || char2 > 'Z') { 674 throw new IllegalArgumentException(); 675 } 676 return mainTable[(char1 - 'A') * A_TO_Z + (char2 - 'A')]; 677 } 678 679 /** 680 * Sets the main table entry for the country whose country code consists 681 * of char1 and char2. 682 */ 683 private static void setMainTableEntry(char char1, char char2, int entry) { 684 if (char1 < 'A' || char1 > 'Z' || char2 < 'A' || char2 > 'Z') { 685 throw new IllegalArgumentException(); 686 } 687 mainTable[(char1 - 'A') * A_TO_Z + (char2 - 'A')] = entry; 688 } 689 690 /** 691 * Obtains a localized currency names from a CurrencyNameProvider 692 * implementation. 693 */ 694 private static class CurrencyNameGetter 695 implements LocaleServiceProviderPool.LocalizedObjectGetter<CurrencyNameProvider, 696 String> { 697 private static final CurrencyNameGetter INSTANCE = new CurrencyNameGetter(); 698 699 @Override 700 public String getObject(CurrencyNameProvider currencyNameProvider, 701 Locale locale, 702 String key, 703 Object... params) { 704 assert params.length == 1; 705 int type = (Integer)params[0]; 706 707 switch(type) { 708 case SYMBOL: 709 return currencyNameProvider.getSymbol(key, locale); 710 case DISPLAYNAME: 711 return currencyNameProvider.getDisplayName(key, locale); 712 default: 713 assert false; // shouldn't happen 714 } 715 716 return null; 717 } 718 } 719 720 private static int[] readIntArray(DataInputStream dis, int count) throws IOException { 721 int[] ret = new int[count]; 722 for (int i = 0; i < count; i++) { 723 ret[i] = dis.readInt(); 724 } 725 726 return ret; 727 } 728 729 private static List<SpecialCaseEntry> readSpecialCases(DataInputStream dis, 730 int count) 731 throws IOException { 732 733 List<SpecialCaseEntry> list = new ArrayList<>(count); 734 long cutOverTime; 735 String oldCurrency; 736 String newCurrency; 737 int oldCurrencyFraction; 738 int newCurrencyFraction; 739 int oldCurrencyNumericCode; 740 int newCurrencyNumericCode; 741 742 for (int i = 0; i < count; i++) { 743 cutOverTime = dis.readLong(); 744 oldCurrency = dis.readUTF(); 745 newCurrency = dis.readUTF(); 746 oldCurrencyFraction = dis.readInt(); 747 newCurrencyFraction = dis.readInt(); 748 oldCurrencyNumericCode = dis.readInt(); 749 newCurrencyNumericCode = dis.readInt(); 750 SpecialCaseEntry sc = new SpecialCaseEntry(cutOverTime, 751 oldCurrency, newCurrency, 752 oldCurrencyFraction, newCurrencyFraction, 753 oldCurrencyNumericCode, newCurrencyNumericCode); 754 list.add(sc); 755 } 756 return list; 757 } 758 759 private static List<OtherCurrencyEntry> readOtherCurrencies(DataInputStream dis, 760 int count) 761 throws IOException { 762 763 List<OtherCurrencyEntry> list = new ArrayList<>(count); 764 String currencyCode; 765 int fraction; 766 int numericCode; 767 768 for (int i = 0; i < count; i++) { 769 currencyCode = dis.readUTF(); 770 fraction = dis.readInt(); 771 numericCode = dis.readInt(); 772 OtherCurrencyEntry oc = new OtherCurrencyEntry(currencyCode, 773 fraction, 774 numericCode); 775 list.add(oc); 776 } 777 return list; 778 } 779 780 /** 781 * Parse currency data found in the properties file (that 782 * java.util.currency.data designates) to a List of CurrencyProperty 783 * instances. Also, remove invalid entries and the multiple currency 784 * code inconsistencies. 785 * 786 * @param props properties containing currency data 787 * @param pattern regex pattern for the properties entry 788 * @return list of parsed property entries 789 */ 790 private static List<CurrencyProperty> getValidCurrencyData(Properties props, 791 Pattern pattern) { 792 793 Set<String> keys = props.stringPropertyNames(); 794 List<CurrencyProperty> propertyEntries = new ArrayList<>(); 795 796 // remove all invalid entries and parse all valid currency properties 797 // entries to a group of CurrencyProperty, classified by currency code 798 Map<String, List<CurrencyProperty>> currencyCodeGroup = keys.stream() 799 .map(k -> CurrencyProperty 800 .getValidEntry(k.toUpperCase(Locale.ROOT), 801 props.getProperty(k).toUpperCase(Locale.ROOT), 802 pattern)).flatMap(o -> o.stream()) 803 .collect(Collectors.groupingBy(entry -> entry.currencyCode)); 804 805 // check each group for inconsistencies 806 currencyCodeGroup.forEach((curCode, list) -> { 807 boolean inconsistent = CurrencyProperty 808 .containsInconsistentInstances(list); 809 if (inconsistent) { 810 list.forEach(prop -> CurrencyProperty.info("The property" 811 + " entry for " + prop.country + " is inconsistent." 812 + " Ignored.", null)); 813 } else { 814 propertyEntries.addAll(list); 815 } 816 }); 817 818 return propertyEntries; 819 } 820 821 /** 822 * Replaces currency data found in the properties file that 823 * java.util.currency.data designates. This method is invoked for 824 * each valid currency entry. 825 * 826 * @param prop CurrencyProperty instance of the valid property entry 827 */ 828 private static void replaceCurrencyData(CurrencyProperty prop) { 829 830 831 String ctry = prop.country; 832 String code = prop.currencyCode; 833 int numeric = prop.numericCode; 834 int fraction = prop.fraction; 835 int entry = numeric << NUMERIC_CODE_SHIFT; 836 837 int index = SpecialCaseEntry.indexOf(code, fraction, numeric); 838 839 840 // If a new entry changes the numeric code/dfd of an existing 841 // currency code, update it in the sc list at the respective 842 // index and also change it in the other currencies list and 843 // main table (if that currency code is also used as a 844 // simple case). 845 846 // If all three components do not match with the new entry, 847 // but the currency code exists in the special case list 848 // update the sc entry with the new entry 849 int scCurrencyCodeIndex = -1; 850 if (index == -1) { 851 scCurrencyCodeIndex = SpecialCaseEntry.currencyCodeIndex(code); 852 if (scCurrencyCodeIndex != -1) { 853 //currency code exists in sc list, then update the old entry 854 specialCasesList.set(scCurrencyCodeIndex, 855 new SpecialCaseEntry(code, fraction, numeric)); 856 857 // also update the entry in other currencies list 858 OtherCurrencyEntry oe = OtherCurrencyEntry.findEntry(code); 859 if (oe != null) { 860 int oIndex = otherCurrenciesList.indexOf(oe); 861 otherCurrenciesList.set(oIndex, new OtherCurrencyEntry( 862 code, fraction, numeric)); 863 } 864 } 865 } 866 867 /* If a country switches from simple case to special case or 868 * one special case to other special case which is not present 869 * in the sc arrays then insert the new entry in special case arrays. 870 * If an entry with given currency code exists, update with the new 871 * entry. 872 */ 873 if (index == -1 && (ctry.charAt(0) != code.charAt(0) 874 || ctry.charAt(1) != code.charAt(1))) { 875 876 if(scCurrencyCodeIndex == -1) { 877 specialCasesList.add(new SpecialCaseEntry(code, fraction, 878 numeric)); 879 index = specialCasesList.size() - 1; 880 } else { 881 index = scCurrencyCodeIndex; 882 } 883 884 // update the entry in main table if it exists as a simple case 885 updateMainTableEntry(code, fraction, numeric); 886 } 887 888 if (index == -1) { 889 // simple case 890 entry |= (fraction << SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT) 891 | (code.charAt(2) - 'A'); 892 } else { 893 // special case 894 entry = SPECIAL_CASE_COUNTRY_MASK 895 | (index + SPECIAL_CASE_COUNTRY_INDEX_DELTA); 896 } 897 setMainTableEntry(ctry.charAt(0), ctry.charAt(1), entry); 898 } 899 900 // update the entry in maintable for any simple case found, if a new 901 // entry as a special case updates the entry in sc list with 902 // existing currency code 903 private static void updateMainTableEntry(String code, int fraction, 904 int numeric) { 905 // checking the existence of currency code in mainTable 906 int tableEntry = getMainTableEntry(code.charAt(0), code.charAt(1)); 907 int entry = numeric << NUMERIC_CODE_SHIFT; 908 if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK 909 && tableEntry != INVALID_COUNTRY_ENTRY 910 && code.charAt(2) - 'A' == (tableEntry 911 & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK)) { 912 913 int numericCode = (tableEntry & NUMERIC_CODE_MASK) 914 >> NUMERIC_CODE_SHIFT; 915 int defaultFractionDigits = (tableEntry 916 & SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK) 917 >> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT; 918 if (numeric != numericCode || fraction != defaultFractionDigits) { 919 // update the entry in main table 920 entry |= (fraction << SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT) 921 | (code.charAt(2) - 'A'); 922 setMainTableEntry(code.charAt(0), code.charAt(1), entry); 923 } 924 } 925 } 926 927 /* Used to represent a special case currency entry 928 * - cutOverTime: cut-over time in millis as returned by 929 * System.currentTimeMillis for special case countries that are changing 930 * currencies; Long.MAX_VALUE for countries that are not changing currencies 931 * - oldCurrency: old currencies for special case countries 932 * - newCurrency: new currencies for special case countries that are 933 * changing currencies; null for others 934 * - oldCurrencyFraction: default fraction digits for old currencies 935 * - newCurrencyFraction: default fraction digits for new currencies, 0 for 936 * countries that are not changing currencies 937 * - oldCurrencyNumericCode: numeric code for old currencies 938 * - newCurrencyNumericCode: numeric code for new currencies, 0 for countries 939 * that are not changing currencies 940 */ 941 private static class SpecialCaseEntry { 942 943 final private long cutOverTime; 944 final private String oldCurrency; 945 final private String newCurrency; 946 final private int oldCurrencyFraction; 947 final private int newCurrencyFraction; 948 final private int oldCurrencyNumericCode; 949 final private int newCurrencyNumericCode; 950 951 private SpecialCaseEntry(long cutOverTime, String oldCurrency, String newCurrency, 952 int oldCurrencyFraction, int newCurrencyFraction, 953 int oldCurrencyNumericCode, int newCurrencyNumericCode) { 954 this.cutOverTime = cutOverTime; 955 this.oldCurrency = oldCurrency; 956 this.newCurrency = newCurrency; 957 this.oldCurrencyFraction = oldCurrencyFraction; 958 this.newCurrencyFraction = newCurrencyFraction; 959 this.oldCurrencyNumericCode = oldCurrencyNumericCode; 960 this.newCurrencyNumericCode = newCurrencyNumericCode; 961 } 962 963 private SpecialCaseEntry(String currencyCode, int fraction, 964 int numericCode) { 965 this(Long.MAX_VALUE, currencyCode, "", fraction, 0, numericCode, 0); 966 } 967 968 //get the index of the special case entry 969 private static int indexOf(String code, int fraction, int numeric) { 970 int size = specialCasesList.size(); 971 for (int index = 0; index < size; index++) { 972 SpecialCaseEntry scEntry = specialCasesList.get(index); 973 if (scEntry.oldCurrency.equals(code) 974 && scEntry.oldCurrencyFraction == fraction 975 && scEntry.oldCurrencyNumericCode == numeric 976 && scEntry.cutOverTime == Long.MAX_VALUE) { 977 return index; 978 } 979 } 980 return -1; 981 } 982 983 // get the fraction and numericCode of the sc currencycode 984 private static int[] findEntry(String code) { 985 int[] fractionAndNumericCode = null; 986 int size = specialCasesList.size(); 987 for (int index = 0; index < size; index++) { 988 SpecialCaseEntry scEntry = specialCasesList.get(index); 989 if (scEntry.oldCurrency.equals(code) && (scEntry.cutOverTime == Long.MAX_VALUE 990 || System.currentTimeMillis() < scEntry.cutOverTime)) { 991 //consider only when there is no new currency or cutover time is not passed 992 fractionAndNumericCode = new int[2]; 993 fractionAndNumericCode[0] = scEntry.oldCurrencyFraction; 994 fractionAndNumericCode[1] = scEntry.oldCurrencyNumericCode; 995 break; 996 } else if (scEntry.newCurrency.equals(code) 997 && System.currentTimeMillis() >= scEntry.cutOverTime) { 998 //consider only if the cutover time is passed 999 fractionAndNumericCode = new int[2]; 1000 fractionAndNumericCode[0] = scEntry.newCurrencyFraction; 1001 fractionAndNumericCode[1] = scEntry.newCurrencyNumericCode; 1002 break; 1003 } 1004 } 1005 return fractionAndNumericCode; 1006 } 1007 1008 // get the index based on currency code 1009 private static int currencyCodeIndex(String code) { 1010 int size = specialCasesList.size(); 1011 for (int index = 0; index < size; index++) { 1012 SpecialCaseEntry scEntry = specialCasesList.get(index); 1013 if (scEntry.oldCurrency.equals(code) && (scEntry.cutOverTime == Long.MAX_VALUE 1014 || System.currentTimeMillis() < scEntry.cutOverTime)) { 1015 //consider only when there is no new currency or cutover time is not passed 1016 return index; 1017 } else if (scEntry.newCurrency.equals(code) 1018 && System.currentTimeMillis() >= scEntry.cutOverTime) { 1019 //consider only if the cutover time is passed 1020 return index; 1021 } 1022 } 1023 return -1; 1024 } 1025 1026 1027 // convert the special case entry to sc arrays index 1028 private static int toIndex(int tableEntry) { 1029 return (tableEntry & SPECIAL_CASE_COUNTRY_INDEX_MASK) - SPECIAL_CASE_COUNTRY_INDEX_DELTA; 1030 } 1031 1032 } 1033 1034 /* Used to represent Other currencies 1035 * - currencyCode: currency codes that are not the main currency 1036 * of a simple country 1037 * - otherCurrenciesDFD: decimal format digits for other currencies 1038 * - otherCurrenciesNumericCode: numeric code for other currencies 1039 */ 1040 private static class OtherCurrencyEntry { 1041 1042 final private String currencyCode; 1043 final private int fraction; 1044 final private int numericCode; 1045 1046 private OtherCurrencyEntry(String currencyCode, int fraction, 1047 int numericCode) { 1048 this.currencyCode = currencyCode; 1049 this.fraction = fraction; 1050 this.numericCode = numericCode; 1051 } 1052 1053 //get the instance of the other currency code 1054 private static OtherCurrencyEntry findEntry(String code) { 1055 int size = otherCurrenciesList.size(); 1056 for (int index = 0; index < size; index++) { 1057 OtherCurrencyEntry ocEntry = otherCurrenciesList.get(index); 1058 if (ocEntry.currencyCode.equalsIgnoreCase(code)) { 1059 return ocEntry; 1060 } 1061 } 1062 return null; 1063 } 1064 1065 } 1066 1067 1068 /* 1069 * Used to represent an entry of the properties file that 1070 * java.util.currency.data designates 1071 * 1072 * - country: country representing the currency entry 1073 * - currencyCode: currency code 1074 * - fraction: default fraction digit 1075 * - numericCode: numeric code 1076 * - date: cutover date 1077 */ 1078 private static class CurrencyProperty { 1079 final private String country; 1080 final private String currencyCode; 1081 final private int fraction; 1082 final private int numericCode; 1083 final private String date; 1084 1085 private CurrencyProperty(String country, String currencyCode, 1086 int fraction, int numericCode, String date) { 1087 this.country = country; 1088 this.currencyCode = currencyCode; 1089 this.fraction = fraction; 1090 this.numericCode = numericCode; 1091 this.date = date; 1092 } 1093 1094 /** 1095 * Check the valid currency data and create/return an Optional instance 1096 * of CurrencyProperty 1097 * 1098 * @param ctry country representing the currency data 1099 * @param curData currency data of the given {@code ctry} 1100 * @param pattern regex pattern for the properties entry 1101 * @return Optional containing CurrencyProperty instance, If valid; 1102 * empty otherwise 1103 */ 1104 private static Optional<CurrencyProperty> getValidEntry(String ctry, 1105 String curData, 1106 Pattern pattern) { 1107 1108 CurrencyProperty prop = null; 1109 1110 if (ctry.length() != 2) { 1111 // Invalid country code. Ignore the entry. 1112 } else { 1113 1114 prop = parseProperty(ctry, curData, pattern); 1115 // if the property entry failed any of the below checked 1116 // criteria it is ignored 1117 if (prop == null 1118 || (prop.date == null && curData.chars() 1119 .map(c -> c == ',' ? 1 : 0).sum() >= 3)) { 1120 // format is not recognized. ignore the data if date 1121 // string is null and we've 4 values, bad date value 1122 prop = null; 1123 } else if (prop.fraction 1124 > SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS) { 1125 prop = null; 1126 } else { 1127 try { 1128 if (prop.date != null 1129 && !isPastCutoverDate(prop.date)) { 1130 prop = null; 1131 } 1132 } catch (ParseException ex) { 1133 prop = null; 1134 } 1135 } 1136 } 1137 1138 if (prop == null) { 1139 info("The property entry for " + ctry + " is invalid." 1140 + " Ignored.", null); 1141 } 1142 1143 return Optional.ofNullable(prop); 1144 } 1145 1146 /* 1147 * Parse properties entry and return CurrencyProperty instance 1148 */ 1149 private static CurrencyProperty parseProperty(String ctry, 1150 String curData, Pattern pattern) { 1151 Matcher m = pattern.matcher(curData); 1152 if (!m.find()) { 1153 return null; 1154 } else { 1155 return new CurrencyProperty(ctry, m.group(1), 1156 Integer.parseInt(m.group(3)), 1157 Integer.parseInt(m.group(2)), m.group(4)); 1158 } 1159 } 1160 1161 /** 1162 * Checks if the given list contains multiple inconsistent currency instances 1163 */ 1164 private static boolean containsInconsistentInstances( 1165 List<CurrencyProperty> list) { 1166 int numCode = list.get(0).numericCode; 1167 int fractionDigit = list.get(0).fraction; 1168 return list.stream().anyMatch(prop -> prop.numericCode != numCode 1169 || prop.fraction != fractionDigit); 1170 } 1171 1172 private static boolean isPastCutoverDate(String s) 1173 throws ParseException { 1174 SimpleDateFormat format = new SimpleDateFormat( 1175 "yyyy-MM-dd'T'HH:mm:ss", Locale.ROOT); 1176 format.setTimeZone(TimeZone.getTimeZone("UTC")); 1177 format.setLenient(false); 1178 long time = format.parse(s.trim()).getTime(); 1179 return System.currentTimeMillis() > time; 1180 1181 } 1182 1183 private static void info(String message, Throwable t) { 1184 PlatformLogger logger = PlatformLogger 1185 .getLogger("java.util.Currency"); 1186 if (logger.isLoggable(PlatformLogger.Level.INFO)) { 1187 if (t != null) { 1188 logger.info(message, t); 1189 } else { 1190 logger.info(message); 1191 } 1192 } 1193 } 1194 1195 } 1196 1197 } 1198 1199