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