Path: blob/master/src/java.base/share/classes/java/util/Currency.java
41152 views
/*1* Copyright (c) 2000, 2021, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425package java.util;2627import java.io.BufferedInputStream;28import java.io.DataInputStream;29import java.io.File;30import java.io.FileReader;31import java.io.InputStream;32import java.io.IOException;33import java.io.Serializable;34import java.security.AccessController;35import java.security.PrivilegedAction;36import java.text.ParseException;37import java.text.SimpleDateFormat;38import java.util.concurrent.ConcurrentHashMap;39import java.util.concurrent.ConcurrentMap;40import java.util.regex.Pattern;41import java.util.regex.Matcher;42import java.util.spi.CurrencyNameProvider;43import java.util.stream.Collectors;4445import jdk.internal.util.StaticProperty;46import sun.util.locale.provider.CalendarDataUtility;47import sun.util.locale.provider.LocaleServiceProviderPool;48import sun.util.logging.PlatformLogger;495051/**52* Represents a currency. Currencies are identified by their ISO 4217 currency53* codes. Visit the <a href="http://www.iso.org/iso/home/standards/currency_codes.htm">54* ISO web site</a> for more information.55* <p>56* The class is designed so that there's never more than one57* {@code Currency} instance for any given currency. Therefore, there's58* no public constructor. You obtain a {@code Currency} instance using59* the {@code getInstance} methods.60* <p>61* Users can supersede the Java runtime currency data by means of the system62* property {@systemProperty java.util.currency.data}. If this system property is63* defined then its value is the location of a properties file, the contents of64* which are key/value pairs of the ISO 3166 country codes and the ISO 421765* currency data respectively. The value part consists of three ISO 4217 values66* of a currency, i.e., an alphabetic code, a numeric code, and a minor unit.67* Those three ISO 4217 values are separated by commas.68* The lines which start with '#'s are considered comment lines. An optional UTC69* timestamp may be specified per currency entry if users need to specify a70* cutover date indicating when the new data comes into effect. The timestamp is71* appended to the end of the currency properties and uses a comma as a separator.72* If a UTC datestamp is present and valid, the JRE will only use the new currency73* properties if the current UTC date is later than the date specified at class74* loading time. The format of the timestamp must be of ISO 8601 format :75* {@code 'yyyy-MM-dd'T'HH:mm:ss'}. For example,76* <p>77* <code>78* #Sample currency properties<br>79* JP=JPZ,999,080* </code>81* <p>82* will supersede the currency data for Japan. If JPZ is one of the existing83* ISO 4217 currency code referred by other countries, the existing84* JPZ currency data is updated with the given numeric code and minor85* unit value.86*87* <p>88* <code>89* #Sample currency properties with cutover date<br>90* JP=JPZ,999,0,2014-01-01T00:00:0091* </code>92* <p>93* will supersede the currency data for Japan if {@code Currency} class is loaded after94* 1st January 2014 00:00:00 GMT.95* <p>96* Where syntactically malformed entries are encountered, the entry is ignored97* and the remainder of entries in file are processed. For instances where duplicate98* country code entries exist, the behavior of the Currency information for that99* {@code Currency} is undefined and the remainder of entries in file are processed.100* <p>101* If multiple property entries with same currency code but different numeric code102* and/or minor unit are encountered, those entries are ignored and the remainder103* of entries in file are processed.104*105* <p>106* It is recommended to use {@link java.math.BigDecimal} class while dealing107* with {@code Currency} or monetary values as it provides better handling of floating108* point numbers and their operations.109*110* @see java.math.BigDecimal111* @since 1.4112*/113@SuppressWarnings("removal")114public final class Currency implements Serializable {115116@java.io.Serial117private static final long serialVersionUID = -158308464356906721L;118119/**120* ISO 4217 currency code for this currency.121*122* @serial123*/124private final String currencyCode;125126/**127* Default fraction digits for this currency.128* Set from currency data tables.129*/130private final transient int defaultFractionDigits;131132/**133* ISO 4217 numeric code for this currency.134* Set from currency data tables.135*/136private final transient int numericCode;137138139// class data: instance map140141private static ConcurrentMap<String, Currency> instances = new ConcurrentHashMap<>(7);142private static HashSet<Currency> available;143144// Class data: currency data obtained from currency.data file.145// Purpose:146// - determine valid country codes147// - determine valid currency codes148// - map country codes to currency codes149// - obtain default fraction digits for currency codes150//151// sc = special case; dfd = default fraction digits152// Simple countries are those where the country code is a prefix of the153// 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 int158// - 26*26 entries, corresponding to [A-Z]*[A-Z]159// - \u007F -> not valid country160// - bits 20-31: unused161// - bits 10-19: numeric code (0 to 1023)162// - bit 9: 1 - special case, bits 0-4 indicate which one163// 0 - simple country, bits 0-4 indicate final char of currency code164// - bits 5-8: fraction digits for simple countries, 0 for special cases165// - bits 0-4: final char for currency code for simple country, or ID of special case166// - special case IDs:167// - 0: country has no currency168// - other: index into specialCasesList169170static int formatVersion;171static int dataVersion;172static int[] mainTable;173static List<SpecialCaseEntry> specialCasesList;174static List<OtherCurrencyEntry> otherCurrenciesList;175176// handy constants - must match definitions in GenerateCurrencyData177// magic number178private static final int MAGIC_NUMBER = 0x43757244;179// number of characters from A to Z180private static final int A_TO_Z = ('Z' - 'A') + 1;181// entry for invalid country codes182private static final int INVALID_COUNTRY_ENTRY = 0x0000007F;183// entry for countries without currency184private static final int COUNTRY_WITHOUT_CURRENCY_ENTRY = 0x00000200;185// mask for simple case country entries186private static final int SIMPLE_CASE_COUNTRY_MASK = 0x00000000;187// mask for simple case country entry final character188private static final int SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK = 0x0000001F;189// mask for simple case country entry default currency digits190private static final int SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK = 0x000001E0;191// shift count for simple case country entry default currency digits192private static final int SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT = 5;193// maximum number for simple case country entry default currency digits194private static final int SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS = 9;195// mask for special case country entries196private static final int SPECIAL_CASE_COUNTRY_MASK = 0x00000200;197// mask for special case country index198private static final int SPECIAL_CASE_COUNTRY_INDEX_MASK = 0x0000001F;199// delta from entry index component in main table to index into special case tables200private static final int SPECIAL_CASE_COUNTRY_INDEX_DELTA = 1;201// mask for distinguishing simple and special case countries202private static final int COUNTRY_TYPE_MASK = SIMPLE_CASE_COUNTRY_MASK | SPECIAL_CASE_COUNTRY_MASK;203// mask for the numeric code of the currency204private static final int NUMERIC_CODE_MASK = 0x000FFC00;205// shift count for the numeric code of the currency206private static final int NUMERIC_CODE_SHIFT = 10;207208// Currency data format version209private static final int VALID_FORMAT_VERSION = 3;210211static {212AccessController.doPrivileged(new PrivilegedAction<>() {213@Override214public Void run() {215try {216try (InputStream in = getClass().getResourceAsStream("/java/util/currency.data")) {217if (in == null) {218throw new InternalError("Currency data not found");219}220DataInputStream dis = new DataInputStream(new BufferedInputStream(in));221if (dis.readInt() != MAGIC_NUMBER) {222throw new InternalError("Currency data is possibly corrupted");223}224formatVersion = dis.readInt();225if (formatVersion != VALID_FORMAT_VERSION) {226throw new InternalError("Currency data format is incorrect");227}228dataVersion = dis.readInt();229mainTable = readIntArray(dis, A_TO_Z * A_TO_Z);230int scCount = dis.readInt();231specialCasesList = readSpecialCases(dis, scCount);232int ocCount = dis.readInt();233otherCurrenciesList = readOtherCurrencies(dis, ocCount);234}235} catch (IOException e) {236throw new InternalError(e);237}238239// look for the properties file for overrides240String propsFile = System.getProperty("java.util.currency.data");241if (propsFile == null) {242propsFile = StaticProperty.javaHome() + File.separator + "lib" +243File.separator + "currency.properties";244}245try {246File propFile = new File(propsFile);247if (propFile.exists()) {248Properties props = new Properties();249try (FileReader fr = new FileReader(propFile)) {250props.load(fr);251}252Pattern propertiesPattern =253Pattern.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})?");256List<CurrencyProperty> currencyEntries257= getValidCurrencyData(props, propertiesPattern);258currencyEntries.forEach(Currency::replaceCurrencyData);259}260} catch (IOException e) {261CurrencyProperty.info("currency.properties is ignored"262+ " because of an IOException", e);263}264return null;265}266});267}268269/**270* Constants for retrieving localized names from the name providers.271*/272private static final int SYMBOL = 0;273private static final int DISPLAYNAME = 1;274275276/**277* Constructs a {@code Currency} instance. The constructor is private278* so that we can insure that there's never more than one instance for a279* given currency.280*/281private Currency(String currencyCode, int defaultFractionDigits, int numericCode) {282this.currencyCode = currencyCode;283this.defaultFractionDigits = defaultFractionDigits;284this.numericCode = numericCode;285}286287/**288* Returns the {@code Currency} instance for the given currency code.289*290* @param currencyCode the ISO 4217 code of the currency291* @return the {@code Currency} instance for the given currency code292* @throws NullPointerException if {@code currencyCode} is null293* @throws IllegalArgumentException if {@code currencyCode} is not294* a supported ISO 4217 code.295*/296public static Currency getInstance(String currencyCode) {297return getInstance(currencyCode, Integer.MIN_VALUE, 0);298}299300private static Currency getInstance(String currencyCode, int defaultFractionDigits,301int 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.305Currency instance = instances.get(currencyCode);306if (instance != null) {307return instance;308}309310if (defaultFractionDigits == Integer.MIN_VALUE) {311// Currency code not internally generated, need to verify first312// A currency code must have 3 characters and exist in the main table313// or in the list of other currencies.314boolean found = false;315if (currencyCode.length() != 3) {316throw new IllegalArgumentException();317}318char char1 = currencyCode.charAt(0);319char char2 = currencyCode.charAt(1);320int tableEntry = getMainTableEntry(char1, char2);321if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK322&& tableEntry != INVALID_COUNTRY_ENTRY323&& currencyCode.charAt(2) - 'A' == (tableEntry & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK)) {324defaultFractionDigits = (tableEntry & SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK) >> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT;325numericCode = (tableEntry & NUMERIC_CODE_MASK) >> NUMERIC_CODE_SHIFT;326found = true;327} else { //special case328int[] fractionAndNumericCode = SpecialCaseEntry.findEntry(currencyCode);329if (fractionAndNumericCode != null) {330defaultFractionDigits = fractionAndNumericCode[0];331numericCode = fractionAndNumericCode[1];332found = true;333}334}335336if (!found) {337OtherCurrencyEntry ocEntry = OtherCurrencyEntry.findEntry(currencyCode);338if (ocEntry == null) {339throw new IllegalArgumentException();340}341defaultFractionDigits = ocEntry.fraction;342numericCode = ocEntry.numericCode;343}344}345346Currency currencyVal =347new Currency(currencyCode, defaultFractionDigits, numericCode);348instance = instances.putIfAbsent(currencyCode, currencyVal);349return (instance != null ? instance : currencyVal);350}351352/**353* Returns the {@code Currency} instance for the country of the354* given locale. The language and variant components of the locale355* are ignored. The result may vary over time, as countries change their356* currencies. For example, for the original member countries of the357* European Monetary Union, the method returns the old national currencies358* until December 31, 2001, and the Euro from January 1, 2002, local time359* 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 reflects364* the values specified with those extensions. If both "cu" and "rg" are365* specified, the currency from the "cu" extension supersedes the implicit one366* from the "rg" extension.367* <p>368* The method returns {@code null} for territories that don't369* have a currency, such as Antarctica.370*371* @param locale the locale for whose country a {@code Currency}372* instance is needed373* @return the {@code Currency} instance for the country of the given374* locale, or {@code null}375* @throws NullPointerException if {@code locale}376* is {@code null}377* @throws IllegalArgumentException if the country of the given {@code locale}378* is not a supported ISO 3166 country code.379*/380public static Currency getInstance(Locale locale) {381// check for locale overrides382String override = locale.getUnicodeLocaleType("cu");383if (override != null) {384try {385return getInstance(override.toUpperCase(Locale.ROOT));386} catch (IllegalArgumentException iae) {387// override currency is invalid. Fall through.388}389}390391String country = CalendarDataUtility.findRegionOverride(locale).getCountry();392393if (country == null || !country.matches("^[a-zA-Z]{2}$")) {394throw new IllegalArgumentException();395}396397char char1 = country.charAt(0);398char char2 = country.charAt(1);399int tableEntry = getMainTableEntry(char1, char2);400if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK401&& tableEntry != INVALID_COUNTRY_ENTRY) {402char finalChar = (char) ((tableEntry & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK) + 'A');403int defaultFractionDigits = (tableEntry & SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK) >> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT;404int numericCode = (tableEntry & NUMERIC_CODE_MASK) >> NUMERIC_CODE_SHIFT;405StringBuilder sb = new StringBuilder(country);406sb.append(finalChar);407return getInstance(sb.toString(), defaultFractionDigits, numericCode);408} else {409// special cases410if (tableEntry == INVALID_COUNTRY_ENTRY) {411throw new IllegalArgumentException();412}413if (tableEntry == COUNTRY_WITHOUT_CURRENCY_ENTRY) {414return null;415} else {416int index = SpecialCaseEntry.toIndex(tableEntry);417SpecialCaseEntry scEntry = specialCasesList.get(index);418if (scEntry.cutOverTime == Long.MAX_VALUE419|| System.currentTimeMillis() < scEntry.cutOverTime) {420return getInstance(scEntry.oldCurrency,421scEntry.oldCurrencyFraction,422scEntry.oldCurrencyNumericCode);423} else {424return getInstance(scEntry.newCurrency,425scEntry.newCurrencyFraction,426scEntry.newCurrencyNumericCode);427}428}429}430}431432/**433* Gets the set of available currencies. The returned set of currencies434* contains all of the available currencies, which may include currencies435* that represent obsolete ISO 4217 codes. The set can be modified436* without affecting the available currencies in the runtime.437*438* @return the set of available currencies. If there is no currency439* available in the runtime, the returned set is empty.440* @since 1.7441*/442public static Set<Currency> getAvailableCurrencies() {443synchronized(Currency.class) {444if (available == null) {445available = new HashSet<>(256);446447// Add simple currencies first448for (char c1 = 'A'; c1 <= 'Z'; c1 ++) {449for (char c2 = 'A'; c2 <= 'Z'; c2 ++) {450int tableEntry = getMainTableEntry(c1, c2);451if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK452&& tableEntry != INVALID_COUNTRY_ENTRY) {453char finalChar = (char) ((tableEntry & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK) + 'A');454int defaultFractionDigits = (tableEntry & SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK) >> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT;455int numericCode = (tableEntry & NUMERIC_CODE_MASK) >> NUMERIC_CODE_SHIFT;456StringBuilder sb = new StringBuilder();457sb.append(c1);458sb.append(c2);459sb.append(finalChar);460available.add(getInstance(sb.toString(), defaultFractionDigits, numericCode));461} else if ((tableEntry & COUNTRY_TYPE_MASK) == SPECIAL_CASE_COUNTRY_MASK462&& tableEntry != INVALID_COUNTRY_ENTRY463&& tableEntry != COUNTRY_WITHOUT_CURRENCY_ENTRY) {464int index = SpecialCaseEntry.toIndex(tableEntry);465SpecialCaseEntry scEntry = specialCasesList.get(index);466467if (scEntry.cutOverTime == Long.MAX_VALUE468|| System.currentTimeMillis() < scEntry.cutOverTime) {469available.add(getInstance(scEntry.oldCurrency,470scEntry.oldCurrencyFraction,471scEntry.oldCurrencyNumericCode));472} else {473available.add(getInstance(scEntry.newCurrency,474scEntry.newCurrencyFraction,475scEntry.newCurrencyNumericCode));476}477}478}479}480481// Now add other currencies482for (OtherCurrencyEntry entry : otherCurrenciesList) {483available.add(getInstance(entry.currencyCode));484}485}486}487488@SuppressWarnings("unchecked")489Set<Currency> result = (Set<Currency>) available.clone();490return result;491}492493/**494* Gets the ISO 4217 currency code of this currency.495*496* @return the ISO 4217 currency code of this currency.497*/498public String getCurrencyCode() {499return currencyCode;500}501502/**503* Gets the symbol of this currency for the default504* {@link Locale.Category#DISPLAY DISPLAY} locale.505* For example, for the US Dollar, the symbol is "$" if the default506* locale is the US, while for other locales it may be "US$". If no507* symbol can be determined, the ISO 4217 currency code is returned.508* <p>509* If the default {@link Locale.Category#DISPLAY DISPLAY} locale510* contains "rg" (region override)511* <a href="./Locale.html#def_locale_extension">Unicode extension</a>,512* the symbol returned from this method reflects513* the value specified with that extension.514* <p>515* This is equivalent to calling516* {@link #getSymbol(Locale)517* getSymbol(Locale.getDefault(Locale.Category.DISPLAY))}.518*519* @return the symbol of this currency for the default520* {@link Locale.Category#DISPLAY DISPLAY} locale521*/522public String getSymbol() {523return getSymbol(Locale.getDefault(Locale.Category.DISPLAY));524}525526/**527* Gets the symbol of this currency for the specified locale.528* For example, for the US Dollar, the symbol is "$" if the specified529* locale is the US, while for other locales it may be "US$". If no530* 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 reflects535* the value specified with that extension.536*537* @param locale the locale for which a display name for this currency is538* needed539* @return the symbol of this currency for the specified locale540* @throws NullPointerException if {@code locale} is null541*/542public String getSymbol(Locale locale) {543LocaleServiceProviderPool pool =544LocaleServiceProviderPool.getPool(CurrencyNameProvider.class);545locale = CalendarDataUtility.findRegionOverride(locale);546String symbol = pool.getLocalizedObject(547CurrencyNameGetter.INSTANCE,548locale, currencyCode, SYMBOL);549if (symbol != null) {550return symbol;551}552553// use currency code as symbol of last resort554return currencyCode;555}556557/**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's560* 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 currency567*/568public int getDefaultFractionDigits() {569return defaultFractionDigits;570}571572/**573* Returns the ISO 4217 numeric code of this currency.574*575* @return the ISO 4217 numeric code of this currency576* @since 1.7577*/578public int getNumericCode() {579return numericCode;580}581582/**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 9591*/592public String getNumericCodeAsString() {593/* numeric code could be returned as a 3 digit string simply by using594String.format("%03d",numericCode); which uses regex to parse the format,595"%03d" in this case. Parsing a regex gives an extra performance overhead,596so String.format() approach is avoided in this scenario.597*/598if (numericCode < 100) {599StringBuilder sb = new StringBuilder();600sb.append('0');601if (numericCode < 10) {602sb.append('0');603}604return sb.append(numericCode).toString();605}606return String.valueOf(numericCode);607}608609/**610* Gets the name that is suitable for displaying this currency for611* the default {@link Locale.Category#DISPLAY DISPLAY} locale.612* If there is no suitable display name found613* for the default locale, the ISO 4217 currency code is returned.614* <p>615* This is equivalent to calling616* {@link #getDisplayName(Locale)617* getDisplayName(Locale.getDefault(Locale.Category.DISPLAY))}.618*619* @return the display name of this currency for the default620* {@link Locale.Category#DISPLAY DISPLAY} locale621* @since 1.7622*/623public String getDisplayName() {624return getDisplayName(Locale.getDefault(Locale.Category.DISPLAY));625}626627/**628* Gets the name that is suitable for displaying this currency for629* the specified locale. If there is no suitable display name found630* 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 is633* needed634* @return the display name of this currency for the specified locale635* @throws NullPointerException if {@code locale} is null636* @since 1.7637*/638public String getDisplayName(Locale locale) {639LocaleServiceProviderPool pool =640LocaleServiceProviderPool.getPool(CurrencyNameProvider.class);641String result = pool.getLocalizedObject(642CurrencyNameGetter.INSTANCE,643locale, currencyCode, DISPLAYNAME);644if (result != null) {645return result;646}647648// use currency code as symbol of last resort649return currencyCode;650}651652/**653* Returns the ISO 4217 currency code of this currency.654*655* @return the ISO 4217 currency code of this currency656*/657@Override658public String toString() {659return currencyCode;660}661662/**663* Resolves instances being deserialized to a single instance per currency.664*/665@java.io.Serial666private Object readResolve() {667return getInstance(currencyCode);668}669670/**671* Gets the main table entry for the country whose country code consists672* of char1 and char2.673*/674private static int getMainTableEntry(char char1, char char2) {675if (char1 < 'A' || char1 > 'Z' || char2 < 'A' || char2 > 'Z') {676throw new IllegalArgumentException();677}678return mainTable[(char1 - 'A') * A_TO_Z + (char2 - 'A')];679}680681/**682* Sets the main table entry for the country whose country code consists683* of char1 and char2.684*/685private static void setMainTableEntry(char char1, char char2, int entry) {686if (char1 < 'A' || char1 > 'Z' || char2 < 'A' || char2 > 'Z') {687throw new IllegalArgumentException();688}689mainTable[(char1 - 'A') * A_TO_Z + (char2 - 'A')] = entry;690}691692/**693* Obtains a localized currency names from a CurrencyNameProvider694* implementation.695*/696private static class CurrencyNameGetter697implements LocaleServiceProviderPool.LocalizedObjectGetter<CurrencyNameProvider,698String> {699private static final CurrencyNameGetter INSTANCE = new CurrencyNameGetter();700701@Override702public String getObject(CurrencyNameProvider currencyNameProvider,703Locale locale,704String key,705Object... params) {706assert params.length == 1;707int type = (Integer)params[0];708709switch(type) {710case SYMBOL:711return currencyNameProvider.getSymbol(key, locale);712case DISPLAYNAME:713return currencyNameProvider.getDisplayName(key, locale);714default:715assert false; // shouldn't happen716}717718return null;719}720}721722private static int[] readIntArray(DataInputStream dis, int count) throws IOException {723int[] ret = new int[count];724for (int i = 0; i < count; i++) {725ret[i] = dis.readInt();726}727728return ret;729}730731private static List<SpecialCaseEntry> readSpecialCases(DataInputStream dis,732int count)733throws IOException {734735List<SpecialCaseEntry> list = new ArrayList<>(count);736long cutOverTime;737String oldCurrency;738String newCurrency;739int oldCurrencyFraction;740int newCurrencyFraction;741int oldCurrencyNumericCode;742int newCurrencyNumericCode;743744for (int i = 0; i < count; i++) {745cutOverTime = dis.readLong();746oldCurrency = dis.readUTF();747newCurrency = dis.readUTF();748oldCurrencyFraction = dis.readInt();749newCurrencyFraction = dis.readInt();750oldCurrencyNumericCode = dis.readInt();751newCurrencyNumericCode = dis.readInt();752SpecialCaseEntry sc = new SpecialCaseEntry(cutOverTime,753oldCurrency, newCurrency,754oldCurrencyFraction, newCurrencyFraction,755oldCurrencyNumericCode, newCurrencyNumericCode);756list.add(sc);757}758return list;759}760761private static List<OtherCurrencyEntry> readOtherCurrencies(DataInputStream dis,762int count)763throws IOException {764765List<OtherCurrencyEntry> list = new ArrayList<>(count);766String currencyCode;767int fraction;768int numericCode;769770for (int i = 0; i < count; i++) {771currencyCode = dis.readUTF();772fraction = dis.readInt();773numericCode = dis.readInt();774OtherCurrencyEntry oc = new OtherCurrencyEntry(currencyCode,775fraction,776numericCode);777list.add(oc);778}779return list;780}781782/**783* Parse currency data found in the properties file (that784* java.util.currency.data designates) to a List of CurrencyProperty785* instances. Also, remove invalid entries and the multiple currency786* code inconsistencies.787*788* @param props properties containing currency data789* @param pattern regex pattern for the properties entry790* @return list of parsed property entries791*/792private static List<CurrencyProperty> getValidCurrencyData(Properties props,793Pattern pattern) {794795Set<String> keys = props.stringPropertyNames();796List<CurrencyProperty> propertyEntries = new ArrayList<>();797798// remove all invalid entries and parse all valid currency properties799// entries to a group of CurrencyProperty, classified by currency code800Map<String, List<CurrencyProperty>> currencyCodeGroup = keys.stream()801.map(k -> CurrencyProperty802.getValidEntry(k.toUpperCase(Locale.ROOT),803props.getProperty(k).toUpperCase(Locale.ROOT),804pattern)).flatMap(o -> o.stream())805.collect(Collectors.groupingBy(entry -> entry.currencyCode));806807// check each group for inconsistencies808currencyCodeGroup.forEach((curCode, list) -> {809boolean inconsistent = CurrencyProperty810.containsInconsistentInstances(list);811if (inconsistent) {812list.forEach(prop -> CurrencyProperty.info("The property"813+ " entry for " + prop.country + " is inconsistent."814+ " Ignored.", null));815} else {816propertyEntries.addAll(list);817}818});819820return propertyEntries;821}822823/**824* Replaces currency data found in the properties file that825* java.util.currency.data designates. This method is invoked for826* each valid currency entry.827*828* @param prop CurrencyProperty instance of the valid property entry829*/830private static void replaceCurrencyData(CurrencyProperty prop) {831832833String ctry = prop.country;834String code = prop.currencyCode;835int numeric = prop.numericCode;836int fraction = prop.fraction;837int entry = numeric << NUMERIC_CODE_SHIFT;838839int index = SpecialCaseEntry.indexOf(code, fraction, numeric);840841842// If a new entry changes the numeric code/dfd of an existing843// currency code, update it in the sc list at the respective844// index and also change it in the other currencies list and845// main table (if that currency code is also used as a846// simple case).847848// If all three components do not match with the new entry,849// but the currency code exists in the special case list850// update the sc entry with the new entry851int scCurrencyCodeIndex = -1;852if (index == -1) {853scCurrencyCodeIndex = SpecialCaseEntry.currencyCodeIndex(code);854if (scCurrencyCodeIndex != -1) {855//currency code exists in sc list, then update the old entry856specialCasesList.set(scCurrencyCodeIndex,857new SpecialCaseEntry(code, fraction, numeric));858859// also update the entry in other currencies list860OtherCurrencyEntry oe = OtherCurrencyEntry.findEntry(code);861if (oe != null) {862int oIndex = otherCurrenciesList.indexOf(oe);863otherCurrenciesList.set(oIndex, new OtherCurrencyEntry(864code, fraction, numeric));865}866}867}868869/* If a country switches from simple case to special case or870* one special case to other special case which is not present871* 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 new873* entry.874*/875if (index == -1 && (ctry.charAt(0) != code.charAt(0)876|| ctry.charAt(1) != code.charAt(1))) {877878if(scCurrencyCodeIndex == -1) {879specialCasesList.add(new SpecialCaseEntry(code, fraction,880numeric));881index = specialCasesList.size() - 1;882} else {883index = scCurrencyCodeIndex;884}885886// update the entry in main table if it exists as a simple case887updateMainTableEntry(code, fraction, numeric);888}889890if (index == -1) {891// simple case892entry |= (fraction << SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT)893| (code.charAt(2) - 'A');894} else {895// special case896entry = SPECIAL_CASE_COUNTRY_MASK897| (index + SPECIAL_CASE_COUNTRY_INDEX_DELTA);898}899setMainTableEntry(ctry.charAt(0), ctry.charAt(1), entry);900}901902// update the entry in maintable for any simple case found, if a new903// entry as a special case updates the entry in sc list with904// existing currency code905private static void updateMainTableEntry(String code, int fraction,906int numeric) {907// checking the existence of currency code in mainTable908int tableEntry = getMainTableEntry(code.charAt(0), code.charAt(1));909int entry = numeric << NUMERIC_CODE_SHIFT;910if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK911&& tableEntry != INVALID_COUNTRY_ENTRY912&& code.charAt(2) - 'A' == (tableEntry913& SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK)) {914915int numericCode = (tableEntry & NUMERIC_CODE_MASK)916>> NUMERIC_CODE_SHIFT;917int defaultFractionDigits = (tableEntry918& SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK)919>> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT;920if (numeric != numericCode || fraction != defaultFractionDigits) {921// update the entry in main table922entry |= (fraction << SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT)923| (code.charAt(2) - 'A');924setMainTableEntry(code.charAt(0), code.charAt(1), entry);925}926}927}928929/* Used to represent a special case currency entry930* - cutOverTime: cut-over time in millis as returned by931* System.currentTimeMillis for special case countries that are changing932* currencies; Long.MAX_VALUE for countries that are not changing currencies933* - oldCurrency: old currencies for special case countries934* - newCurrency: new currencies for special case countries that are935* changing currencies; null for others936* - oldCurrencyFraction: default fraction digits for old currencies937* - newCurrencyFraction: default fraction digits for new currencies, 0 for938* countries that are not changing currencies939* - oldCurrencyNumericCode: numeric code for old currencies940* - newCurrencyNumericCode: numeric code for new currencies, 0 for countries941* that are not changing currencies942*/943private static class SpecialCaseEntry {944945private final long cutOverTime;946private final String oldCurrency;947private final String newCurrency;948private final int oldCurrencyFraction;949private final int newCurrencyFraction;950private final int oldCurrencyNumericCode;951private final int newCurrencyNumericCode;952953private SpecialCaseEntry(long cutOverTime, String oldCurrency, String newCurrency,954int oldCurrencyFraction, int newCurrencyFraction,955int oldCurrencyNumericCode, int newCurrencyNumericCode) {956this.cutOverTime = cutOverTime;957this.oldCurrency = oldCurrency;958this.newCurrency = newCurrency;959this.oldCurrencyFraction = oldCurrencyFraction;960this.newCurrencyFraction = newCurrencyFraction;961this.oldCurrencyNumericCode = oldCurrencyNumericCode;962this.newCurrencyNumericCode = newCurrencyNumericCode;963}964965private SpecialCaseEntry(String currencyCode, int fraction,966int numericCode) {967this(Long.MAX_VALUE, currencyCode, "", fraction, 0, numericCode, 0);968}969970//get the index of the special case entry971private static int indexOf(String code, int fraction, int numeric) {972int size = specialCasesList.size();973for (int index = 0; index < size; index++) {974SpecialCaseEntry scEntry = specialCasesList.get(index);975if (scEntry.oldCurrency.equals(code)976&& scEntry.oldCurrencyFraction == fraction977&& scEntry.oldCurrencyNumericCode == numeric978&& scEntry.cutOverTime == Long.MAX_VALUE) {979return index;980}981}982return -1;983}984985// get the fraction and numericCode of the sc currencycode986private static int[] findEntry(String code) {987int[] fractionAndNumericCode = null;988int size = specialCasesList.size();989for (int index = 0; index < size; index++) {990SpecialCaseEntry scEntry = specialCasesList.get(index);991if (scEntry.oldCurrency.equals(code) && (scEntry.cutOverTime == Long.MAX_VALUE992|| System.currentTimeMillis() < scEntry.cutOverTime)) {993//consider only when there is no new currency or cutover time is not passed994fractionAndNumericCode = new int[2];995fractionAndNumericCode[0] = scEntry.oldCurrencyFraction;996fractionAndNumericCode[1] = scEntry.oldCurrencyNumericCode;997break;998} else if (scEntry.newCurrency.equals(code)999&& System.currentTimeMillis() >= scEntry.cutOverTime) {1000//consider only if the cutover time is passed1001fractionAndNumericCode = new int[2];1002fractionAndNumericCode[0] = scEntry.newCurrencyFraction;1003fractionAndNumericCode[1] = scEntry.newCurrencyNumericCode;1004break;1005}1006}1007return fractionAndNumericCode;1008}10091010// get the index based on currency code1011private static int currencyCodeIndex(String code) {1012int size = specialCasesList.size();1013for (int index = 0; index < size; index++) {1014SpecialCaseEntry scEntry = specialCasesList.get(index);1015if (scEntry.oldCurrency.equals(code) && (scEntry.cutOverTime == Long.MAX_VALUE1016|| System.currentTimeMillis() < scEntry.cutOverTime)) {1017//consider only when there is no new currency or cutover time is not passed1018return index;1019} else if (scEntry.newCurrency.equals(code)1020&& System.currentTimeMillis() >= scEntry.cutOverTime) {1021//consider only if the cutover time is passed1022return index;1023}1024}1025return -1;1026}102710281029// convert the special case entry to sc arrays index1030private static int toIndex(int tableEntry) {1031return (tableEntry & SPECIAL_CASE_COUNTRY_INDEX_MASK) - SPECIAL_CASE_COUNTRY_INDEX_DELTA;1032}10331034}10351036/* Used to represent Other currencies1037* - currencyCode: currency codes that are not the main currency1038* of a simple country1039* - otherCurrenciesDFD: decimal format digits for other currencies1040* - otherCurrenciesNumericCode: numeric code for other currencies1041*/1042private static class OtherCurrencyEntry {10431044private final String currencyCode;1045private final int fraction;1046private final int numericCode;10471048private OtherCurrencyEntry(String currencyCode, int fraction,1049int numericCode) {1050this.currencyCode = currencyCode;1051this.fraction = fraction;1052this.numericCode = numericCode;1053}10541055//get the instance of the other currency code1056private static OtherCurrencyEntry findEntry(String code) {1057int size = otherCurrenciesList.size();1058for (int index = 0; index < size; index++) {1059OtherCurrencyEntry ocEntry = otherCurrenciesList.get(index);1060if (ocEntry.currencyCode.equalsIgnoreCase(code)) {1061return ocEntry;1062}1063}1064return null;1065}10661067}106810691070/*1071* Used to represent an entry of the properties file that1072* java.util.currency.data designates1073*1074* - country: country representing the currency entry1075* - currencyCode: currency code1076* - fraction: default fraction digit1077* - numericCode: numeric code1078* - date: cutover date1079*/1080private static class CurrencyProperty {1081private final String country;1082private final String currencyCode;1083private final int fraction;1084private final int numericCode;1085private final String date;10861087private CurrencyProperty(String country, String currencyCode,1088int fraction, int numericCode, String date) {1089this.country = country;1090this.currencyCode = currencyCode;1091this.fraction = fraction;1092this.numericCode = numericCode;1093this.date = date;1094}10951096/**1097* Check the valid currency data and create/return an Optional instance1098* of CurrencyProperty1099*1100* @param ctry country representing the currency data1101* @param curData currency data of the given {@code ctry}1102* @param pattern regex pattern for the properties entry1103* @return Optional containing CurrencyProperty instance, If valid;1104* empty otherwise1105*/1106private static Optional<CurrencyProperty> getValidEntry(String ctry,1107String curData,1108Pattern pattern) {11091110CurrencyProperty prop = null;11111112if (ctry.length() != 2) {1113// Invalid country code. Ignore the entry.1114} else {11151116prop = parseProperty(ctry, curData, pattern);1117// if the property entry failed any of the below checked1118// criteria it is ignored1119if (prop == null1120|| (prop.date == null && curData.chars()1121.map(c -> c == ',' ? 1 : 0).sum() >= 3)) {1122// format is not recognized. ignore the data if date1123// string is null and we've 4 values, bad date value1124prop = null;1125} else if (prop.fraction1126> SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS) {1127prop = null;1128} else {1129try {1130if (prop.date != null1131&& !isPastCutoverDate(prop.date)) {1132prop = null;1133}1134} catch (ParseException ex) {1135prop = null;1136}1137}1138}11391140if (prop == null) {1141info("The property entry for " + ctry + " is invalid."1142+ " Ignored.", null);1143}11441145return Optional.ofNullable(prop);1146}11471148/*1149* Parse properties entry and return CurrencyProperty instance1150*/1151private static CurrencyProperty parseProperty(String ctry,1152String curData, Pattern pattern) {1153Matcher m = pattern.matcher(curData);1154if (!m.find()) {1155return null;1156} else {1157return new CurrencyProperty(ctry, m.group(1),1158Integer.parseInt(m.group(3)),1159Integer.parseInt(m.group(2)), m.group(4));1160}1161}11621163/**1164* Checks if the given list contains multiple inconsistent currency instances1165*/1166private static boolean containsInconsistentInstances(1167List<CurrencyProperty> list) {1168int numCode = list.get(0).numericCode;1169int fractionDigit = list.get(0).fraction;1170return list.stream().anyMatch(prop -> prop.numericCode != numCode1171|| prop.fraction != fractionDigit);1172}11731174private static boolean isPastCutoverDate(String s)1175throws ParseException {1176SimpleDateFormat format = new SimpleDateFormat(1177"yyyy-MM-dd'T'HH:mm:ss", Locale.ROOT);1178format.setTimeZone(TimeZone.getTimeZone("UTC"));1179format.setLenient(false);1180long time = format.parse(s.trim()).getTime();1181return System.currentTimeMillis() > time;11821183}11841185private static void info(String message, Throwable t) {1186PlatformLogger logger = PlatformLogger1187.getLogger("java.util.Currency");1188if (logger.isLoggable(PlatformLogger.Level.INFO)) {1189if (t != null) {1190logger.info(message, t);1191} else {1192logger.info(message);1193}1194}1195}11961197}11981199}120012011202