src/java.base/share/classes/java/util/Currency.java
changeset 48929 28d8fc8cd3cd
parent 48251 57148c79bd75
child 50817 fa1e04811ff6
equal deleted inserted replaced
48928:cc30928a834e 48929:28d8fc8cd3cd
     1 /*
     1 /*
     2  * Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.
     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.
     3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
     4  *
     4  *
     5  * This code is free software; you can redistribute it and/or modify it
     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
     6  * under the terms of the GNU General Public License version 2 only, as
     7  * published by the Free Software Foundation.  Oracle designates this
     7  * published by the Free Software Foundation.  Oracle designates this
    39 import java.util.concurrent.ConcurrentHashMap;
    39 import java.util.concurrent.ConcurrentHashMap;
    40 import java.util.concurrent.ConcurrentMap;
    40 import java.util.concurrent.ConcurrentMap;
    41 import java.util.regex.Pattern;
    41 import java.util.regex.Pattern;
    42 import java.util.regex.Matcher;
    42 import java.util.regex.Matcher;
    43 import java.util.spi.CurrencyNameProvider;
    43 import java.util.spi.CurrencyNameProvider;
       
    44 import java.util.stream.Collectors;
    44 import sun.util.locale.provider.CalendarDataUtility;
    45 import sun.util.locale.provider.CalendarDataUtility;
    45 import sun.util.locale.provider.LocaleServiceProviderPool;
    46 import sun.util.locale.provider.LocaleServiceProviderPool;
    46 import sun.util.logging.PlatformLogger;
    47 import sun.util.logging.PlatformLogger;
    47 
    48 
    48 
    49 
    75  * <code>
    76  * <code>
    76  * #Sample currency properties<br>
    77  * #Sample currency properties<br>
    77  * JP=JPZ,999,0
    78  * JP=JPZ,999,0
    78  * </code>
    79  * </code>
    79  * <p>
    80  * <p>
    80  * will supersede the currency data for Japan.
    81  * will supersede the currency data for Japan. If JPZ is one of the existing
       
    82  * ISO 4217 currency code referred by other countries, the existing
       
    83  * JPZ currency data is updated with the given numeric code and minor
       
    84  * unit value.
    81  *
    85  *
    82  * <p>
    86  * <p>
    83  * <code>
    87  * <code>
    84  * #Sample currency properties with cutover date<br>
    88  * #Sample currency properties with cutover date<br>
    85  * JP=JPZ,999,0,2014-01-01T00:00:00
    89  * JP=JPZ,999,0,2014-01-01T00:00:00
    90  * <p>
    94  * <p>
    91  * Where syntactically malformed entries are encountered, the entry is ignored
    95  * Where syntactically malformed entries are encountered, the entry is ignored
    92  * and the remainder of entries in file are processed. For instances where duplicate
    96  * and the remainder of entries in file are processed. For instances where duplicate
    93  * country code entries exist, the behavior of the Currency information for that
    97  * country code entries exist, the behavior of the Currency information for that
    94  * {@code Currency} is undefined and the remainder of entries in file are processed.
    98  * {@code Currency} is undefined and the remainder of entries in file are processed.
       
    99  * <p>
       
   100  * If multiple property entries with same currency code but different numeric code
       
   101  * and/or minor unit are encountered, those entries are ignored and the remainder
       
   102  * of entries in file are processed.
       
   103  *
    95  * <p>
   104  * <p>
    96  * It is recommended to use {@link java.math.BigDecimal} class while dealing
   105  * It is recommended to use {@link java.math.BigDecimal} class while dealing
    97  * with {@code Currency} or monetary values as it provides better handling of floating
   106  * with {@code Currency} or monetary values as it provides better handling of floating
    98  * point numbers and their operations.
   107  * point numbers and their operations.
    99  *
   108  *
   235                     if (propFile.exists()) {
   244                     if (propFile.exists()) {
   236                         Properties props = new Properties();
   245                         Properties props = new Properties();
   237                         try (FileReader fr = new FileReader(propFile)) {
   246                         try (FileReader fr = new FileReader(propFile)) {
   238                             props.load(fr);
   247                             props.load(fr);
   239                         }
   248                         }
   240                         Set<String> keys = props.stringPropertyNames();
       
   241                         Pattern propertiesPattern =
   249                         Pattern propertiesPattern =
   242                             Pattern.compile("([A-Z]{3})\\s*,\\s*(\\d{3})\\s*,\\s*" +
   250                                 Pattern.compile("([A-Z]{3})\\s*,\\s*(\\d{3})\\s*,\\s*" +
   243                                 "(\\d+)\\s*,?\\s*(\\d{4}-\\d{2}-\\d{2}T\\d{2}:" +
   251                                         "(\\d+)\\s*,?\\s*(\\d{4}-\\d{2}-\\d{2}T\\d{2}:" +
   244                                 "\\d{2}:\\d{2})?");
   252                                         "\\d{2}:\\d{2})?");
   245                         for (String key : keys) {
   253                         List<CurrencyProperty> currencyEntries
   246                            replaceCurrencyData(propertiesPattern,
   254                                 = getValidCurrencyData(props, propertiesPattern);
   247                                key.toUpperCase(Locale.ROOT),
   255                         currencyEntries.forEach(Currency::replaceCurrencyData);
   248                                props.getProperty(key).toUpperCase(Locale.ROOT));
       
   249                         }
       
   250                     }
   256                     }
   251                 } catch (IOException e) {
   257                 } catch (IOException e) {
   252                     info("currency.properties is ignored because of an IOException", e);
   258                     CurrencyProperty.info("currency.properties is ignored"
       
   259                             + " because of an IOException", e);
   253                 }
   260                 }
   254                 return null;
   261                 return null;
   255             }
   262             }
   256         });
   263         });
   257     }
   264     }
   767         }
   774         }
   768         return list;
   775         return list;
   769     }
   776     }
   770 
   777 
   771     /**
   778     /**
   772      * Replaces currency data found in the currencydata.properties file
   779      * Parse currency data found in the properties file (that
   773      *
   780      * java.util.currency.data designates) to a List of CurrencyProperty
   774      * @param pattern regex pattern for the properties
   781      * instances. Also, remove invalid entries and the multiple currency
   775      * @param ctry country code
   782      * code inconsistencies.
   776      * @param curdata currency data.  This is a comma separated string that
   783      *
   777      *    consists of "three-letter alphabet code", "three-digit numeric code",
   784      * @param props properties containing currency data
   778      *    and "one-digit (0-9) default fraction digit".
   785      * @param pattern regex pattern for the properties entry
   779      *    For example, "JPZ,392,0".
   786      * @return list of parsed property entries
   780      *    An optional UTC date can be appended to the string (comma separated)
   787      */
   781      *    to allow a currency change take effect after date specified.
   788     private static List<CurrencyProperty> getValidCurrencyData(Properties props,
   782      *    For example, "JP=JPZ,999,0,2014-01-01T00:00:00" has no effect unless
   789             Pattern pattern) {
   783      *    UTC time is past 1st January 2014 00:00:00 GMT.
   790 
   784      */
   791         Set<String> keys = props.stringPropertyNames();
   785     private static void replaceCurrencyData(Pattern pattern, String ctry, String curdata) {
   792         List<CurrencyProperty> propertyEntries = new ArrayList<>();
   786 
   793 
   787         if (ctry.length() != 2) {
   794         // remove all invalid entries and parse all valid currency properties
   788             // ignore invalid country code
   795         // entries to a group of CurrencyProperty, classified by currency code
   789             info("currency.properties entry for " + ctry +
   796         Map<String, List<CurrencyProperty>> currencyCodeGroup = keys.stream()
   790                     " is ignored because of the invalid country code.", null);
   797                 .map(k -> CurrencyProperty
   791             return;
   798                 .getValidEntry(k.toUpperCase(Locale.ROOT),
   792         }
   799                         props.getProperty(k).toUpperCase(Locale.ROOT),
   793 
   800                         pattern)).flatMap(o -> o.stream())
   794         Matcher m = pattern.matcher(curdata);
   801                 .collect(Collectors.groupingBy(entry -> entry.currencyCode));
   795         if (!m.find() || (m.group(4) == null && countOccurrences(curdata, ',') >= 3)) {
   802 
   796             // format is not recognized.  ignore the data
   803         // check each group for inconsistencies
   797             // if group(4) date string is null and we've 4 values, bad date value
   804         currencyCodeGroup.forEach((curCode, list) -> {
   798             info("currency.properties entry for " + ctry +
   805             boolean inconsistent = CurrencyProperty
   799                     " ignored because the value format is not recognized.", null);
   806                     .containsInconsistentInstances(list);
   800             return;
   807             if (inconsistent) {
   801         }
   808                 list.forEach(prop -> CurrencyProperty.info("The property"
   802 
   809                         + " entry for " + prop.country + " is inconsistent."
   803         try {
   810                         + " Ignored.", null));
   804             if (m.group(4) != null && !isPastCutoverDate(m.group(4))) {
   811             } else {
   805                 info("currency.properties entry for " + ctry +
   812                 propertyEntries.addAll(list);
   806                         " ignored since cutover date has not passed :" + curdata, null);
   813             }
   807                 return;
   814         });
   808             }
   815 
   809         } catch (ParseException ex) {
   816         return propertyEntries;
   810             info("currency.properties entry for " + ctry +
   817     }
   811                         " ignored since exception encountered :" + ex.getMessage(), null);
   818 
   812             return;
   819     /**
   813         }
   820      * Replaces currency data found in the properties file that
   814 
   821      * java.util.currency.data designates. This method is invoked for
   815         String code = m.group(1);
   822      * each valid currency entry.
   816         int numeric = Integer.parseInt(m.group(2));
   823      *
       
   824      * @param prop CurrencyProperty instance of the valid property entry
       
   825      */
       
   826     private static void replaceCurrencyData(CurrencyProperty prop) {
       
   827 
       
   828 
       
   829         String ctry = prop.country;
       
   830         String code = prop.currencyCode;
       
   831         int numeric = prop.numericCode;
       
   832         int fraction = prop.fraction;
   817         int entry = numeric << NUMERIC_CODE_SHIFT;
   833         int entry = numeric << NUMERIC_CODE_SHIFT;
   818         int fraction = Integer.parseInt(m.group(3));
       
   819         if (fraction > SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS) {
       
   820             info("currency.properties entry for " + ctry +
       
   821                 " ignored since the fraction is more than " +
       
   822                 SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS + ":" + curdata, null);
       
   823             return;
       
   824         }
       
   825 
   834 
   826         int index = SpecialCaseEntry.indexOf(code, fraction, numeric);
   835         int index = SpecialCaseEntry.indexOf(code, fraction, numeric);
   827 
   836 
   828         /* if a country switches from simple case to special case or
   837 
       
   838         // If a new entry changes the numeric code/dfd of an existing
       
   839         // currency code, update it in the sc list at the respective
       
   840         // index and also change it in the other currencies list and
       
   841         // main table (if that currency code is also used as a
       
   842         // simple case).
       
   843 
       
   844         // If all three components do not match with the new entry,
       
   845         // but the currency code exists in the special case list
       
   846         // update the sc entry with the new entry
       
   847         int scCurrencyCodeIndex = -1;
       
   848         if (index == -1) {
       
   849             scCurrencyCodeIndex = SpecialCaseEntry.currencyCodeIndex(code);
       
   850             if (scCurrencyCodeIndex != -1) {
       
   851                 //currency code exists in sc list, then update the old entry
       
   852                 specialCasesList.set(scCurrencyCodeIndex,
       
   853                         new SpecialCaseEntry(code, fraction, numeric));
       
   854 
       
   855                 // also update the entry in other currencies list
       
   856                 OtherCurrencyEntry oe = OtherCurrencyEntry.findEntry(code);
       
   857                 if (oe != null) {
       
   858                     int oIndex = otherCurrenciesList.indexOf(oe);
       
   859                     otherCurrenciesList.set(oIndex, new OtherCurrencyEntry(
       
   860                             code, fraction, numeric));
       
   861                 }
       
   862             }
       
   863         }
       
   864 
       
   865         /* If a country switches from simple case to special case or
   829          * one special case to other special case which is not present
   866          * one special case to other special case which is not present
   830          * in the sc arrays then insert the new entry in special case arrays
   867          * in the sc arrays then insert the new entry in special case arrays.
       
   868          * If an entry with given currency code exists, update with the new
       
   869          * entry.
   831          */
   870          */
   832         if (index == -1 && (ctry.charAt(0) != code.charAt(0)
   871         if (index == -1 && (ctry.charAt(0) != code.charAt(0)
   833                 || ctry.charAt(1) != code.charAt(1))) {
   872                 || ctry.charAt(1) != code.charAt(1))) {
   834 
   873 
   835             specialCasesList.add(new SpecialCaseEntry(code, fraction, numeric));
   874             if(scCurrencyCodeIndex == -1) {
   836             index = specialCasesList.size() - 1;
   875                 specialCasesList.add(new SpecialCaseEntry(code, fraction,
       
   876                         numeric));
       
   877                 index = specialCasesList.size() - 1;
       
   878             } else {
       
   879                 index = scCurrencyCodeIndex;
       
   880             }
       
   881 
       
   882             // update the entry in main table if it exists as a simple case
       
   883             updateMainTableEntry(code, fraction, numeric);
   837         }
   884         }
   838 
   885 
   839         if (index == -1) {
   886         if (index == -1) {
   840             // simple case
   887             // simple case
   841             entry |= (fraction << SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT)
   888             entry |= (fraction << SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT)
   846                     | (index + SPECIAL_CASE_COUNTRY_INDEX_DELTA);
   893                     | (index + SPECIAL_CASE_COUNTRY_INDEX_DELTA);
   847         }
   894         }
   848         setMainTableEntry(ctry.charAt(0), ctry.charAt(1), entry);
   895         setMainTableEntry(ctry.charAt(0), ctry.charAt(1), entry);
   849     }
   896     }
   850 
   897 
   851     private static boolean isPastCutoverDate(String s) throws ParseException {
   898     // update the entry in maintable for any simple case found, if a new
   852         SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ROOT);
   899     // entry as a special case updates the entry in sc list with
   853         format.setTimeZone(TimeZone.getTimeZone("UTC"));
   900     // existing currency code
   854         format.setLenient(false);
   901     private static void updateMainTableEntry(String code, int fraction,
   855         long time = format.parse(s.trim()).getTime();
   902             int numeric) {
   856         return System.currentTimeMillis() > time;
   903         // checking the existence of currency code in mainTable
   857 
   904         int tableEntry = getMainTableEntry(code.charAt(0), code.charAt(1));
   858     }
   905         int entry = numeric << NUMERIC_CODE_SHIFT;
   859 
   906         if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK
   860     private static int countOccurrences(String value, char match) {
   907                 && tableEntry != INVALID_COUNTRY_ENTRY
   861         int count = 0;
   908                 && code.charAt(2) - 'A' == (tableEntry
   862         for (char c : value.toCharArray()) {
   909                 & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK)) {
   863             if (c == match) {
   910 
   864                ++count;
   911             int numericCode = (tableEntry & NUMERIC_CODE_MASK)
   865             }
   912                     >> NUMERIC_CODE_SHIFT;
   866         }
   913             int defaultFractionDigits = (tableEntry
   867         return count;
   914                     & SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK)
   868     }
   915                     >> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT;
   869 
   916             if (numeric != numericCode || fraction != defaultFractionDigits) {
   870     private static void info(String message, Throwable t) {
   917                 // update the entry in main table
   871         PlatformLogger logger = PlatformLogger.getLogger("java.util.Currency");
   918                 entry |= (fraction << SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT)
   872         if (logger.isLoggable(PlatformLogger.Level.INFO)) {
   919                         | (code.charAt(2) - 'A');
   873             if (t != null) {
   920                 setMainTableEntry(code.charAt(0), code.charAt(1), entry);
   874                 logger.info(message, t);
       
   875             } else {
       
   876                 logger.info(message);
       
   877             }
   921             }
   878         }
   922         }
   879     }
   923     }
   880 
   924 
   881     /* Used to represent a special case currency entry
   925     /* Used to represent a special case currency entry
   957                 }
  1001                 }
   958             }
  1002             }
   959             return fractionAndNumericCode;
  1003             return fractionAndNumericCode;
   960         }
  1004         }
   961 
  1005 
       
  1006         // get the index based on currency code
       
  1007         private static int currencyCodeIndex(String code) {
       
  1008             int size = specialCasesList.size();
       
  1009             for (int index = 0; index < size; index++) {
       
  1010                 SpecialCaseEntry scEntry = specialCasesList.get(index);
       
  1011                 if (scEntry.oldCurrency.equals(code) && (scEntry.cutOverTime == Long.MAX_VALUE
       
  1012                         || System.currentTimeMillis() < scEntry.cutOverTime)) {
       
  1013                     //consider only when there is no new currency or cutover time is not passed
       
  1014                     return index;
       
  1015                 } else if (scEntry.newCurrency.equals(code)
       
  1016                         && System.currentTimeMillis() >= scEntry.cutOverTime) {
       
  1017                     //consider only if the cutover time is passed
       
  1018                     return index;
       
  1019                 }
       
  1020             }
       
  1021             return -1;
       
  1022         }
       
  1023 
       
  1024 
   962         // convert the special case entry to sc arrays index
  1025         // convert the special case entry to sc arrays index
   963         private static int toIndex(int tableEntry) {
  1026         private static int toIndex(int tableEntry) {
   964             return (tableEntry & SPECIAL_CASE_COUNTRY_INDEX_MASK) - SPECIAL_CASE_COUNTRY_INDEX_DELTA;
  1027             return (tableEntry & SPECIAL_CASE_COUNTRY_INDEX_MASK) - SPECIAL_CASE_COUNTRY_INDEX_DELTA;
   965         }
  1028         }
   966 
  1029 
   997             return null;
  1060             return null;
   998         }
  1061         }
   999 
  1062 
  1000     }
  1063     }
  1001 
  1064 
       
  1065 
       
  1066     /*
       
  1067      * Used to represent an entry of the properties file that
       
  1068      * java.util.currency.data designates
       
  1069      *
       
  1070      * - country: country representing the currency entry
       
  1071      * - currencyCode: currency code
       
  1072      * - fraction: default fraction digit
       
  1073      * - numericCode: numeric code
       
  1074      * - date: cutover date
       
  1075      */
       
  1076     private static class CurrencyProperty {
       
  1077         final private String country;
       
  1078         final private String currencyCode;
       
  1079         final private int fraction;
       
  1080         final private int numericCode;
       
  1081         final private String date;
       
  1082 
       
  1083         private CurrencyProperty(String country, String currencyCode,
       
  1084                 int fraction, int numericCode, String date) {
       
  1085             this.country = country;
       
  1086             this.currencyCode = currencyCode;
       
  1087             this.fraction = fraction;
       
  1088             this.numericCode = numericCode;
       
  1089             this.date = date;
       
  1090         }
       
  1091 
       
  1092         /**
       
  1093          * Check the valid currency data and create/return an Optional instance
       
  1094          * of CurrencyProperty
       
  1095          *
       
  1096          * @param ctry    country representing the currency data
       
  1097          * @param curData currency data of the given {@code ctry}
       
  1098          * @param pattern regex pattern for the properties entry
       
  1099          * @return Optional containing CurrencyProperty instance, If valid;
       
  1100          *         empty otherwise
       
  1101          */
       
  1102         private static Optional<CurrencyProperty> getValidEntry(String ctry,
       
  1103                 String curData,
       
  1104                 Pattern pattern) {
       
  1105 
       
  1106             CurrencyProperty prop = null;
       
  1107 
       
  1108             if (ctry.length() != 2) {
       
  1109                 // Invalid country code. Ignore the entry.
       
  1110             } else {
       
  1111 
       
  1112                 prop = parseProperty(ctry, curData, pattern);
       
  1113                 // if the property entry failed any of the below checked
       
  1114                 // criteria it is ignored
       
  1115                 if (prop == null
       
  1116                         || (prop.date == null && curData.chars()
       
  1117                                 .map(c -> c == ',' ? 1 : 0).sum() >= 3)) {
       
  1118                     // format is not recognized.  ignore the data if date
       
  1119                     // string is null and we've 4 values, bad date value
       
  1120                     prop = null;
       
  1121                 } else if (prop.fraction
       
  1122                         > SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS) {
       
  1123                     prop = null;
       
  1124                 } else {
       
  1125                     try {
       
  1126                         if (prop.date != null
       
  1127                                 && !isPastCutoverDate(prop.date)) {
       
  1128                             prop = null;
       
  1129                         }
       
  1130                     } catch (ParseException ex) {
       
  1131                         prop = null;
       
  1132                     }
       
  1133                 }
       
  1134             }
       
  1135 
       
  1136             if (prop == null) {
       
  1137                 info("The property entry for " + ctry + " is invalid."
       
  1138                         + " Ignored.", null);
       
  1139             }
       
  1140 
       
  1141             return Optional.ofNullable(prop);
       
  1142         }
       
  1143 
       
  1144         /*
       
  1145          * Parse properties entry and return CurrencyProperty instance
       
  1146          */
       
  1147         private static CurrencyProperty parseProperty(String ctry,
       
  1148                 String curData, Pattern pattern) {
       
  1149             Matcher m = pattern.matcher(curData);
       
  1150             if (!m.find()) {
       
  1151                 return null;
       
  1152             } else {
       
  1153                 return new CurrencyProperty(ctry, m.group(1),
       
  1154                         Integer.parseInt(m.group(3)),
       
  1155                         Integer.parseInt(m.group(2)), m.group(4));
       
  1156             }
       
  1157         }
       
  1158 
       
  1159         /**
       
  1160          * Checks if the given list contains multiple inconsistent currency instances
       
  1161          */
       
  1162         private static boolean containsInconsistentInstances(
       
  1163                 List<CurrencyProperty> list) {
       
  1164             int numCode = list.get(0).numericCode;
       
  1165             int fractionDigit = list.get(0).fraction;
       
  1166             return list.stream().anyMatch(prop -> prop.numericCode != numCode
       
  1167                     || prop.fraction != fractionDigit);
       
  1168         }
       
  1169 
       
  1170         private static boolean isPastCutoverDate(String s)
       
  1171                 throws ParseException {
       
  1172             SimpleDateFormat format = new SimpleDateFormat(
       
  1173                     "yyyy-MM-dd'T'HH:mm:ss", Locale.ROOT);
       
  1174             format.setTimeZone(TimeZone.getTimeZone("UTC"));
       
  1175             format.setLenient(false);
       
  1176             long time = format.parse(s.trim()).getTime();
       
  1177             return System.currentTimeMillis() > time;
       
  1178 
       
  1179         }
       
  1180 
       
  1181         private static void info(String message, Throwable t) {
       
  1182             PlatformLogger logger = PlatformLogger
       
  1183                     .getLogger("java.util.Currency");
       
  1184             if (logger.isLoggable(PlatformLogger.Level.INFO)) {
       
  1185                 if (t != null) {
       
  1186                     logger.info(message, t);
       
  1187                 } else {
       
  1188                     logger.info(message);
       
  1189                 }
       
  1190             }
       
  1191         }
       
  1192 
       
  1193     }
       
  1194 
  1002 }
  1195 }
  1003 
  1196 
  1004 
  1197