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 |
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 |