View Javadoc
1   /*
2    * Copyright (c) 2000, 2013, 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.FileInputStream;
32  import java.io.FileReader;
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 sun.util.locale.provider.LocaleServiceProviderPool;
45  import sun.util.logging.PlatformLogger;
46  
47  
48  /**
49   * Represents a currency. Currencies are identified by their ISO 4217 currency
50   * codes. Visit the <a href="http://www.iso.org/iso/home/standards/currency_codes.htm">
51   * ISO web site</a> for more information.
52   * <p>
53   * The class is designed so that there's never more than one
54   * <code>Currency</code> instance for any given currency. Therefore, there's
55   * no public constructor. You obtain a <code>Currency</code> instance using
56   * the <code>getInstance</code> methods.
57   * <p>
58   * Users can supersede the Java runtime currency data by means of the system
59   * property {@code java.util.currency.data}. If this system property is
60   * defined then its value is the location of a properties file, the contents of
61   * which are key/value pairs of the ISO 3166 country codes and the ISO 4217
62   * currency data respectively.  The value part consists of three ISO 4217 values
63   * of a currency, i.e., an alphabetic code, a numeric code, and a minor unit.
64   * Those three ISO 4217 values are separated by commas.
65   * The lines which start with '#'s are considered comment lines. An optional UTC
66   * timestamp may be specified per currency entry if users need to specify a
67   * cutover date indicating when the new data comes into effect. The timestamp is
68   * appended to the end of the currency properties and uses a comma as a separator.
69   * If a UTC datestamp is present and valid, the JRE will only use the new currency
70   * properties if the current UTC date is later than the date specified at class
71   * loading time. The format of the timestamp must be of ISO 8601 format :
72   * {@code 'yyyy-MM-dd'T'HH:mm:ss'}. For example,
73   * <p>
74   * <code>
75   * #Sample currency properties<br>
76   * JP=JPZ,999,0
77   * </code>
78   * <p>
79   * will supersede the currency data for Japan.
80   *
81   * <p>
82   * <code>
83   * #Sample currency properties with cutover date<br>
84   * JP=JPZ,999,0,2014-01-01T00:00:00
85   * </code>
86   * <p>
87   * will supersede the currency data for Japan if {@code Currency} class is loaded after
88   * 1st January 2014 00:00:00 GMT.
89   * <p>
90   * Where syntactically malformed entries are encountered, the entry is ignored
91   * and the remainder of entries in file are processed. For instances where duplicate
92   * country code entries exist, the behavior of the Currency information for that
93   * {@code Currency} is undefined and the remainder of entries in file are processed.
94   *
95   * @since 1.4
96   */
97  public final class Currency implements Serializable {
98  
99      private static final long serialVersionUID = -158308464356906721L;
100 
101     /**
102      * ISO 4217 currency code for this currency.
103      *
104      * @serial
105      */
106     private final String currencyCode;
107 
108     /**
109      * Default fraction digits for this currency.
110      * Set from currency data tables.
111      */
112     transient private final int defaultFractionDigits;
113 
114     /**
115      * ISO 4217 numeric code for this currency.
116      * Set from currency data tables.
117      */
118     transient private final int numericCode;
119 
120 
121     // class data: instance map
122 
123     private static ConcurrentMap<String, Currency> instances = new ConcurrentHashMap<>(7);
124     private static HashSet<Currency> available;
125 
126     // Class data: currency data obtained from currency.data file.
127     // Purpose:
128     // - determine valid country codes
129     // - determine valid currency codes
130     // - map country codes to currency codes
131     // - obtain default fraction digits for currency codes
132     //
133     // sc = special case; dfd = default fraction digits
134     // Simple countries are those where the country code is a prefix of the
135     // currency code, and there are no known plans to change the currency.
136     //
137     // table formats:
138     // - mainTable:
139     //   - maps country code to 32-bit int
140     //   - 26*26 entries, corresponding to [A-Z]*[A-Z]
141     //   - \u007F -> not valid country
142     //   - bits 18-31: unused
143     //   - bits 8-17: numeric code (0 to 1023)
144     //   - bit 7: 1 - special case, bits 0-4 indicate which one
145     //            0 - simple country, bits 0-4 indicate final char of currency code
146     //   - bits 5-6: fraction digits for simple countries, 0 for special cases
147     //   - bits 0-4: final char for currency code for simple country, or ID of special case
148     // - special case IDs:
149     //   - 0: country has no currency
150     //   - other: index into sc* arrays + 1
151     // - scCutOverTimes: cut-over time in millis as returned by
152     //   System.currentTimeMillis for special case countries that are changing
153     //   currencies; Long.MAX_VALUE for countries that are not changing currencies
154     // - scOldCurrencies: old currencies for special case countries
155     // - scNewCurrencies: new currencies for special case countries that are
156     //   changing currencies; null for others
157     // - scOldCurrenciesDFD: default fraction digits for old currencies
158     // - scNewCurrenciesDFD: default fraction digits for new currencies, 0 for
159     //   countries that are not changing currencies
160     // - otherCurrencies: concatenation of all currency codes that are not the
161     //   main currency of a simple country, separated by "-"
162     // - otherCurrenciesDFD: decimal format digits for currencies in otherCurrencies, same order
163 
164     static int formatVersion;
165     static int dataVersion;
166     static int[] mainTable;
167     static long[] scCutOverTimes;
168     static String[] scOldCurrencies;
169     static String[] scNewCurrencies;
170     static int[] scOldCurrenciesDFD;
171     static int[] scNewCurrenciesDFD;
172     static int[] scOldCurrenciesNumericCode;
173     static int[] scNewCurrenciesNumericCode;
174     static String otherCurrencies;
175     static int[] otherCurrenciesDFD;
176     static int[] otherCurrenciesNumericCode;
177 
178     // handy constants - must match definitions in GenerateCurrencyData
179     // magic number
180     private static final int MAGIC_NUMBER = 0x43757244;
181     // number of characters from A to Z
182     private static final int A_TO_Z = ('Z' - 'A') + 1;
183     // entry for invalid country codes
184     private static final int INVALID_COUNTRY_ENTRY = 0x007F;
185     // entry for countries without currency
186     private static final int COUNTRY_WITHOUT_CURRENCY_ENTRY = 0x0080;
187     // mask for simple case country entries
188     private static final int SIMPLE_CASE_COUNTRY_MASK = 0x0000;
189     // mask for simple case country entry final character
190     private static final int SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK = 0x001F;
191     // mask for simple case country entry default currency digits
192     private static final int SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK = 0x0060;
193     // shift count for simple case country entry default currency digits
194     private static final int SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT = 5;
195     // mask for special case country entries
196     private static final int SPECIAL_CASE_COUNTRY_MASK = 0x0080;
197     // mask for special case country index
198     private static final int SPECIAL_CASE_COUNTRY_INDEX_MASK = 0x001F;
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 = 0x0003FF00;
205     // shift count for the numeric code of the currency
206     private static final int NUMERIC_CODE_SHIFT = 8;
207 
208     // Currency data format version
209     private static final int VALID_FORMAT_VERSION = 1;
210 
211     static {
212         AccessController.doPrivileged(new PrivilegedAction<Void>() {
213             @Override
214             public Void run() {
215                 String homeDir = System.getProperty("java.home");
216                 try {
217                     String dataFile = homeDir + File.separator +
218                             "lib" + File.separator + "currency.data";
219                     try (DataInputStream dis = new DataInputStream(
220                              new BufferedInputStream(
221                              new FileInputStream(dataFile)))) {
222                         if (dis.readInt() != MAGIC_NUMBER) {
223                             throw new InternalError("Currency data is possibly corrupted");
224                         }
225                         formatVersion = dis.readInt();
226                         if (formatVersion != VALID_FORMAT_VERSION) {
227                             throw new InternalError("Currency data format is incorrect");
228                         }
229                         dataVersion = dis.readInt();
230                         mainTable = readIntArray(dis, A_TO_Z * A_TO_Z);
231                         int scCount = dis.readInt();
232                         scCutOverTimes = readLongArray(dis, scCount);
233                         scOldCurrencies = readStringArray(dis, scCount);
234                         scNewCurrencies = readStringArray(dis, scCount);
235                         scOldCurrenciesDFD = readIntArray(dis, scCount);
236                         scNewCurrenciesDFD = readIntArray(dis, scCount);
237                         scOldCurrenciesNumericCode = readIntArray(dis, scCount);
238                         scNewCurrenciesNumericCode = readIntArray(dis, scCount);
239                         int ocCount = dis.readInt();
240                         otherCurrencies = dis.readUTF();
241                         otherCurrenciesDFD = readIntArray(dis, ocCount);
242                         otherCurrenciesNumericCode = readIntArray(dis, ocCount);
243                     }
244                 } catch (IOException e) {
245                     throw new InternalError(e);
246                 }
247 
248                 // look for the properties file for overrides
249                 String propsFile = System.getProperty("java.util.currency.data");
250                 if (propsFile == null) {
251                     propsFile = homeDir + File.separator + "lib" +
252                         File.separator + "currency.properties";
253                 }
254                 try {
255                     File propFile = new File(propsFile);
256                     if (propFile.exists()) {
257                         Properties props = new Properties();
258                         try (FileReader fr = new FileReader(propFile)) {
259                             props.load(fr);
260                         }
261                         Set<String> keys = props.stringPropertyNames();
262                         Pattern propertiesPattern =
263                             Pattern.compile("([A-Z]{3})\\s*,\\s*(\\d{3})\\s*,\\s*" +
264                                 "([0-3])\\s*,?\\s*(\\d{4}-\\d{2}-\\d{2}T\\d{2}:" +
265                                 "\\d{2}:\\d{2})?");
266                         for (String key : keys) {
267                            replaceCurrencyData(propertiesPattern,
268                                key.toUpperCase(Locale.ROOT),
269                                props.getProperty(key).toUpperCase(Locale.ROOT));
270                         }
271                     }
272                 } catch (IOException e) {
273                     info("currency.properties is ignored because of an IOException", e);
274                 }
275                 return null;
276             }
277         });
278     }
279 
280     /**
281      * Constants for retrieving localized names from the name providers.
282      */
283     private static final int SYMBOL = 0;
284     private static final int DISPLAYNAME = 1;
285 
286 
287     /**
288      * Constructs a <code>Currency</code> instance. The constructor is private
289      * so that we can insure that there's never more than one instance for a
290      * given currency.
291      */
292     private Currency(String currencyCode, int defaultFractionDigits, int numericCode) {
293         this.currencyCode = currencyCode;
294         this.defaultFractionDigits = defaultFractionDigits;
295         this.numericCode = numericCode;
296     }
297 
298     /**
299      * Returns the <code>Currency</code> instance for the given currency code.
300      *
301      * @param currencyCode the ISO 4217 code of the currency
302      * @return the <code>Currency</code> instance for the given currency code
303      * @exception NullPointerException if <code>currencyCode</code> is null
304      * @exception IllegalArgumentException if <code>currencyCode</code> is not
305      * a supported ISO 4217 code.
306      */
307     public static Currency getInstance(String currencyCode) {
308         return getInstance(currencyCode, Integer.MIN_VALUE, 0);
309     }
310 
311     private static Currency getInstance(String currencyCode, int defaultFractionDigits,
312         int numericCode) {
313         // Try to look up the currency code in the instances table.
314         // This does the null pointer check as a side effect.
315         // Also, if there already is an entry, the currencyCode must be valid.
316         Currency instance = instances.get(currencyCode);
317         if (instance != null) {
318             return instance;
319         }
320 
321         if (defaultFractionDigits == Integer.MIN_VALUE) {
322             // Currency code not internally generated, need to verify first
323             // A currency code must have 3 characters and exist in the main table
324             // or in the list of other currencies.
325             if (currencyCode.length() != 3) {
326                 throw new IllegalArgumentException();
327             }
328             char char1 = currencyCode.charAt(0);
329             char char2 = currencyCode.charAt(1);
330             int tableEntry = getMainTableEntry(char1, char2);
331             if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK
332                     && tableEntry != INVALID_COUNTRY_ENTRY
333                     && currencyCode.charAt(2) - 'A' == (tableEntry & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK)) {
334                 defaultFractionDigits = (tableEntry & SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK) >> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT;
335                 numericCode = (tableEntry & NUMERIC_CODE_MASK) >> NUMERIC_CODE_SHIFT;
336             } else {
337                 // Check for '-' separately so we don't get false hits in the table.
338                 if (currencyCode.charAt(2) == '-') {
339                     throw new IllegalArgumentException();
340                 }
341                 int index = otherCurrencies.indexOf(currencyCode);
342                 if (index == -1) {
343                     throw new IllegalArgumentException();
344                 }
345                 defaultFractionDigits = otherCurrenciesDFD[index / 4];
346                 numericCode = otherCurrenciesNumericCode[index / 4];
347             }
348         }
349 
350         Currency currencyVal =
351             new Currency(currencyCode, defaultFractionDigits, numericCode);
352         instance = instances.putIfAbsent(currencyCode, currencyVal);
353         return (instance != null ? instance : currencyVal);
354     }
355 
356     /**
357      * Returns the <code>Currency</code> instance for the country of the
358      * given locale. The language and variant components of the locale
359      * are ignored. The result may vary over time, as countries change their
360      * currencies. For example, for the original member countries of the
361      * European Monetary Union, the method returns the old national currencies
362      * until December 31, 2001, and the Euro from January 1, 2002, local time
363      * of the respective countries.
364      * <p>
365      * The method returns <code>null</code> for territories that don't
366      * have a currency, such as Antarctica.
367      *
368      * @param locale the locale for whose country a <code>Currency</code>
369      * instance is needed
370      * @return the <code>Currency</code> instance for the country of the given
371      * locale, or {@code null}
372      * @exception NullPointerException if <code>locale</code> or its country
373      * code is {@code null}
374      * @exception IllegalArgumentException if the country of the given {@code locale}
375      * is not a supported ISO 3166 country code.
376      */
377     public static Currency getInstance(Locale locale) {
378         String country = locale.getCountry();
379         if (country == null) {
380             throw new NullPointerException();
381         }
382 
383         if (country.length() != 2) {
384             throw new IllegalArgumentException();
385         }
386 
387         char char1 = country.charAt(0);
388         char char2 = country.charAt(1);
389         int tableEntry = getMainTableEntry(char1, char2);
390         if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK
391                     && tableEntry != INVALID_COUNTRY_ENTRY) {
392             char finalChar = (char) ((tableEntry & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK) + 'A');
393             int defaultFractionDigits = (tableEntry & SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK) >> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT;
394             int numericCode = (tableEntry & NUMERIC_CODE_MASK) >> NUMERIC_CODE_SHIFT;
395             StringBuilder sb = new StringBuilder(country);
396             sb.append(finalChar);
397             return getInstance(sb.toString(), defaultFractionDigits, numericCode);
398         } else {
399             // special cases
400             if (tableEntry == INVALID_COUNTRY_ENTRY) {
401                 throw new IllegalArgumentException();
402             }
403             if (tableEntry == COUNTRY_WITHOUT_CURRENCY_ENTRY) {
404                 return null;
405             } else {
406                 int index = (tableEntry & SPECIAL_CASE_COUNTRY_INDEX_MASK) - SPECIAL_CASE_COUNTRY_INDEX_DELTA;
407                 if (scCutOverTimes[index] == Long.MAX_VALUE || System.currentTimeMillis() < scCutOverTimes[index]) {
408                     return getInstance(scOldCurrencies[index], scOldCurrenciesDFD[index],
409                         scOldCurrenciesNumericCode[index]);
410                 } else {
411                     return getInstance(scNewCurrencies[index], scNewCurrenciesDFD[index],
412                         scNewCurrenciesNumericCode[index]);
413                 }
414             }
415         }
416     }
417 
418     /**
419      * Gets the set of available currencies.  The returned set of currencies
420      * contains all of the available currencies, which may include currencies
421      * that represent obsolete ISO 4217 codes.  The set can be modified
422      * without affecting the available currencies in the runtime.
423      *
424      * @return the set of available currencies.  If there is no currency
425      *    available in the runtime, the returned set is empty.
426      * @since 1.7
427      */
428     public static Set<Currency> getAvailableCurrencies() {
429         synchronized(Currency.class) {
430             if (available == null) {
431                 available = new HashSet<>(256);
432 
433                 // Add simple currencies first
434                 for (char c1 = 'A'; c1 <= 'Z'; c1 ++) {
435                     for (char c2 = 'A'; c2 <= 'Z'; c2 ++) {
436                         int tableEntry = getMainTableEntry(c1, c2);
437                         if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK
438                              && tableEntry != INVALID_COUNTRY_ENTRY) {
439                             char finalChar = (char) ((tableEntry & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK) + 'A');
440                             int defaultFractionDigits = (tableEntry & SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK) >> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT;
441                             int numericCode = (tableEntry & NUMERIC_CODE_MASK) >> NUMERIC_CODE_SHIFT;
442                             StringBuilder sb = new StringBuilder();
443                             sb.append(c1);
444                             sb.append(c2);
445                             sb.append(finalChar);
446                             available.add(getInstance(sb.toString(), defaultFractionDigits, numericCode));
447                         }
448                     }
449                 }
450 
451                 // Now add other currencies
452                 StringTokenizer st = new StringTokenizer(otherCurrencies, "-");
453                 while (st.hasMoreElements()) {
454                     available.add(getInstance((String)st.nextElement()));
455                 }
456             }
457         }
458 
459         @SuppressWarnings("unchecked")
460         Set<Currency> result = (Set<Currency>) available.clone();
461         return result;
462     }
463 
464     /**
465      * Gets the ISO 4217 currency code of this currency.
466      *
467      * @return the ISO 4217 currency code of this currency.
468      */
469     public String getCurrencyCode() {
470         return currencyCode;
471     }
472 
473     /**
474      * Gets the symbol of this currency for the default
475      * {@link Locale.Category#DISPLAY DISPLAY} locale.
476      * For example, for the US Dollar, the symbol is "$" if the default
477      * locale is the US, while for other locales it may be "US$". If no
478      * symbol can be determined, the ISO 4217 currency code is returned.
479      * <p>
480      * This is equivalent to calling
481      * {@link #getSymbol(Locale)
482      *     getSymbol(Locale.getDefault(Locale.Category.DISPLAY))}.
483      *
484      * @return the symbol of this currency for the default
485      *     {@link Locale.Category#DISPLAY DISPLAY} locale
486      */
487     public String getSymbol() {
488         return getSymbol(Locale.getDefault(Locale.Category.DISPLAY));
489     }
490 
491     /**
492      * Gets the symbol of this currency for the specified locale.
493      * For example, for the US Dollar, the symbol is "$" if the specified
494      * locale is the US, while for other locales it may be "US$". If no
495      * symbol can be determined, the ISO 4217 currency code is returned.
496      *
497      * @param locale the locale for which a display name for this currency is
498      * needed
499      * @return the symbol of this currency for the specified locale
500      * @exception NullPointerException if <code>locale</code> is null
501      */
502     public String getSymbol(Locale locale) {
503         LocaleServiceProviderPool pool =
504             LocaleServiceProviderPool.getPool(CurrencyNameProvider.class);
505         String symbol = pool.getLocalizedObject(
506                                 CurrencyNameGetter.INSTANCE,
507                                 locale, currencyCode, SYMBOL);
508         if (symbol != null) {
509             return symbol;
510         }
511 
512         // use currency code as symbol of last resort
513         return currencyCode;
514     }
515 
516     /**
517      * Gets the default number of fraction digits used with this currency.
518      * For example, the default number of fraction digits for the Euro is 2,
519      * while for the Japanese Yen it's 0.
520      * In the case of pseudo-currencies, such as IMF Special Drawing Rights,
521      * -1 is returned.
522      *
523      * @return the default number of fraction digits used with this currency
524      */
525     public int getDefaultFractionDigits() {
526         return defaultFractionDigits;
527     }
528 
529     /**
530      * Returns the ISO 4217 numeric code of this currency.
531      *
532      * @return the ISO 4217 numeric code of this currency
533      * @since 1.7
534      */
535     public int getNumericCode() {
536         return numericCode;
537     }
538 
539     /**
540      * Gets the name that is suitable for displaying this currency for
541      * the default {@link Locale.Category#DISPLAY DISPLAY} locale.
542      * If there is no suitable display name found
543      * for the default locale, the ISO 4217 currency code is returned.
544      * <p>
545      * This is equivalent to calling
546      * {@link #getDisplayName(Locale)
547      *     getDisplayName(Locale.getDefault(Locale.Category.DISPLAY))}.
548      *
549      * @return the display name of this currency for the default
550      *     {@link Locale.Category#DISPLAY DISPLAY} locale
551      * @since 1.7
552      */
553     public String getDisplayName() {
554         return getDisplayName(Locale.getDefault(Locale.Category.DISPLAY));
555     }
556 
557     /**
558      * Gets the name that is suitable for displaying this currency for
559      * the specified locale.  If there is no suitable display name found
560      * for the specified locale, the ISO 4217 currency code is returned.
561      *
562      * @param locale the locale for which a display name for this currency is
563      * needed
564      * @return the display name of this currency for the specified locale
565      * @exception NullPointerException if <code>locale</code> is null
566      * @since 1.7
567      */
568     public String getDisplayName(Locale locale) {
569         LocaleServiceProviderPool pool =
570             LocaleServiceProviderPool.getPool(CurrencyNameProvider.class);
571         String result = pool.getLocalizedObject(
572                                 CurrencyNameGetter.INSTANCE,
573                                 locale, currencyCode, DISPLAYNAME);
574         if (result != null) {
575             return result;
576         }
577 
578         // use currency code as symbol of last resort
579         return currencyCode;
580     }
581 
582     /**
583      * Returns the ISO 4217 currency code of this currency.
584      *
585      * @return the ISO 4217 currency code of this currency
586      */
587     @Override
588     public String toString() {
589         return currencyCode;
590     }
591 
592     /**
593      * Resolves instances being deserialized to a single instance per currency.
594      */
595     private Object readResolve() {
596         return getInstance(currencyCode);
597     }
598 
599     /**
600      * Gets the main table entry for the country whose country code consists
601      * of char1 and char2.
602      */
603     private static int getMainTableEntry(char char1, char char2) {
604         if (char1 < 'A' || char1 > 'Z' || char2 < 'A' || char2 > 'Z') {
605             throw new IllegalArgumentException();
606         }
607         return mainTable[(char1 - 'A') * A_TO_Z + (char2 - 'A')];
608     }
609 
610     /**
611      * Sets the main table entry for the country whose country code consists
612      * of char1 and char2.
613      */
614     private static void setMainTableEntry(char char1, char char2, int entry) {
615         if (char1 < 'A' || char1 > 'Z' || char2 < 'A' || char2 > 'Z') {
616             throw new IllegalArgumentException();
617         }
618         mainTable[(char1 - 'A') * A_TO_Z + (char2 - 'A')] = entry;
619     }
620 
621     /**
622      * Obtains a localized currency names from a CurrencyNameProvider
623      * implementation.
624      */
625     private static class CurrencyNameGetter
626         implements LocaleServiceProviderPool.LocalizedObjectGetter<CurrencyNameProvider,
627                                                                    String> {
628         private static final CurrencyNameGetter INSTANCE = new CurrencyNameGetter();
629 
630         @Override
631         public String getObject(CurrencyNameProvider currencyNameProvider,
632                                 Locale locale,
633                                 String key,
634                                 Object... params) {
635             assert params.length == 1;
636             int type = (Integer)params[0];
637 
638             switch(type) {
639             case SYMBOL:
640                 return currencyNameProvider.getSymbol(key, locale);
641             case DISPLAYNAME:
642                 return currencyNameProvider.getDisplayName(key, locale);
643             default:
644                 assert false; // shouldn't happen
645             }
646 
647             return null;
648         }
649     }
650 
651     private static int[] readIntArray(DataInputStream dis, int count) throws IOException {
652         int[] ret = new int[count];
653         for (int i = 0; i < count; i++) {
654             ret[i] = dis.readInt();
655         }
656 
657         return ret;
658     }
659 
660     private static long[] readLongArray(DataInputStream dis, int count) throws IOException {
661         long[] ret = new long[count];
662         for (int i = 0; i < count; i++) {
663             ret[i] = dis.readLong();
664         }
665 
666         return ret;
667     }
668 
669     private static String[] readStringArray(DataInputStream dis, int count) throws IOException {
670         String[] ret = new String[count];
671         for (int i = 0; i < count; i++) {
672             ret[i] = dis.readUTF();
673         }
674 
675         return ret;
676     }
677 
678     /**
679      * Replaces currency data found in the currencydata.properties file
680      *
681      * @param pattern regex pattern for the properties
682      * @param ctry country code
683      * @param curdata currency data.  This is a comma separated string that
684      *    consists of "three-letter alphabet code", "three-digit numeric code",
685      *    and "one-digit (0,1,2, or 3) default fraction digit".
686      *    For example, "JPZ,392,0".
687      *    An optional UTC date can be appended to the string (comma separated)
688      *    to allow a currency change take effect after date specified.
689      *    For example, "JP=JPZ,999,0,2014-01-01T00:00:00" has no effect unless
690      *    UTC time is past 1st January 2014 00:00:00 GMT.
691      */
692     private static void replaceCurrencyData(Pattern pattern, String ctry, String curdata) {
693 
694         if (ctry.length() != 2) {
695             // ignore invalid country code
696             info("currency.properties entry for " + ctry +
697                     " is ignored because of the invalid country code.", null);
698             return;
699         }
700 
701         Matcher m = pattern.matcher(curdata);
702         if (!m.find() || (m.group(4) == null && countOccurrences(curdata, ',') >= 3)) {
703             // format is not recognized.  ignore the data
704             // if group(4) date string is null and we've 4 values, bad date value
705             info("currency.properties entry for " + ctry +
706                     " ignored because the value format is not recognized.", null);
707             return;
708         }
709 
710         try {
711             if (m.group(4) != null && !isPastCutoverDate(m.group(4))) {
712                 info("currency.properties entry for " + ctry +
713                         " ignored since cutover date has not passed :" + curdata, null);
714                 return;
715             }
716         } catch (ParseException ex) {
717             info("currency.properties entry for " + ctry +
718                         " ignored since exception encountered :" + ex.getMessage(), null);
719             return;
720         }
721 
722         String code = m.group(1);
723         int numeric = Integer.parseInt(m.group(2));
724         int fraction = Integer.parseInt(m.group(3));
725         int entry = numeric << NUMERIC_CODE_SHIFT;
726 
727         int index;
728         for (index = 0; index < scOldCurrencies.length; index++) {
729             if (scOldCurrencies[index].equals(code)) {
730                 break;
731             }
732         }
733 
734         if (index == scOldCurrencies.length) {
735             // simple case
736             entry |= (fraction << SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT) |
737                      (code.charAt(2) - 'A');
738         } else {
739             // special case
740             entry |= SPECIAL_CASE_COUNTRY_MASK |
741                      (index + SPECIAL_CASE_COUNTRY_INDEX_DELTA);
742         }
743         setMainTableEntry(ctry.charAt(0), ctry.charAt(1), entry);
744     }
745 
746     private static boolean isPastCutoverDate(String s) throws ParseException {
747         SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ROOT);
748         format.setTimeZone(TimeZone.getTimeZone("UTC"));
749         format.setLenient(false);
750         long time = format.parse(s.trim()).getTime();
751         return System.currentTimeMillis() > time;
752 
753     }
754 
755     private static int countOccurrences(String value, char match) {
756         int count = 0;
757         for (char c : value.toCharArray()) {
758             if (c == match) {
759                ++count;
760             }
761         }
762         return count;
763     }
764 
765     private static void info(String message, Throwable t) {
766         PlatformLogger logger = PlatformLogger.getLogger("java.util.Currency");
767         if (logger.isLoggable(PlatformLogger.Level.INFO)) {
768             if (t != null) {
769                 logger.info(message, t);
770             } else {
771                 logger.info(message);
772             }
773         }
774     }
775 }