# HG changeset patch # User nishjain # Date 1519280521 -19800 # Node ID 28d8fc8cd3cd05ad32bd4a37aa22d70d9d017f37 # Parent cc30928a834e327a942caddfe71f459b64198c78 8190904: Incorrect currency instance returned by java.util.Currency.getInstance() Reviewed-by: naoto diff -r cc30928a834e -r 28d8fc8cd3cd src/java.base/share/classes/java/util/Currency.java --- a/src/java.base/share/classes/java/util/Currency.java Wed Feb 21 15:09:40 2018 -0800 +++ b/src/java.base/share/classes/java/util/Currency.java Thu Feb 22 11:52:01 2018 +0530 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -41,6 +41,7 @@ import java.util.regex.Pattern; import java.util.regex.Matcher; import java.util.spi.CurrencyNameProvider; +import java.util.stream.Collectors; import sun.util.locale.provider.CalendarDataUtility; import sun.util.locale.provider.LocaleServiceProviderPool; import sun.util.logging.PlatformLogger; @@ -77,7 +78,10 @@ * JP=JPZ,999,0 * *

- * will supersede the currency data for Japan. + * will supersede the currency data for Japan. If JPZ is one of the existing + * ISO 4217 currency code referred by other countries, the existing + * JPZ currency data is updated with the given numeric code and minor + * unit value. * *

* @@ -93,6 +97,11 @@ * country code entries exist, the behavior of the Currency information for that * {@code Currency} is undefined and the remainder of entries in file are processed. *

+ * If multiple property entries with same currency code but different numeric code + * and/or minor unit are encountered, those entries are ignored and the remainder + * of entries in file are processed. + * + *

* It is recommended to use {@link java.math.BigDecimal} class while dealing * with {@code Currency} or monetary values as it provides better handling of floating * point numbers and their operations. @@ -237,19 +246,17 @@ try (FileReader fr = new FileReader(propFile)) { props.load(fr); } - Set keys = props.stringPropertyNames(); Pattern propertiesPattern = - Pattern.compile("([A-Z]{3})\\s*,\\s*(\\d{3})\\s*,\\s*" + - "(\\d+)\\s*,?\\s*(\\d{4}-\\d{2}-\\d{2}T\\d{2}:" + - "\\d{2}:\\d{2})?"); - for (String key : keys) { - replaceCurrencyData(propertiesPattern, - key.toUpperCase(Locale.ROOT), - props.getProperty(key).toUpperCase(Locale.ROOT)); - } + Pattern.compile("([A-Z]{3})\\s*,\\s*(\\d{3})\\s*,\\s*" + + "(\\d+)\\s*,?\\s*(\\d{4}-\\d{2}-\\d{2}T\\d{2}:" + + "\\d{2}:\\d{2})?"); + List currencyEntries + = getValidCurrencyData(props, propertiesPattern); + currencyEntries.forEach(Currency::replaceCurrencyData); } } catch (IOException e) { - info("currency.properties is ignored because of an IOException", e); + CurrencyProperty.info("currency.properties is ignored" + + " because of an IOException", e); } return null; } @@ -769,71 +776,111 @@ } /** - * Replaces currency data found in the currencydata.properties file + * Parse currency data found in the properties file (that + * java.util.currency.data designates) to a List of CurrencyProperty + * instances. Also, remove invalid entries and the multiple currency + * code inconsistencies. * - * @param pattern regex pattern for the properties - * @param ctry country code - * @param curdata currency data. This is a comma separated string that - * consists of "three-letter alphabet code", "three-digit numeric code", - * and "one-digit (0-9) default fraction digit". - * For example, "JPZ,392,0". - * An optional UTC date can be appended to the string (comma separated) - * to allow a currency change take effect after date specified. - * For example, "JP=JPZ,999,0,2014-01-01T00:00:00" has no effect unless - * UTC time is past 1st January 2014 00:00:00 GMT. + * @param props properties containing currency data + * @param pattern regex pattern for the properties entry + * @return list of parsed property entries */ - private static void replaceCurrencyData(Pattern pattern, String ctry, String curdata) { + private static List getValidCurrencyData(Properties props, + Pattern pattern) { + + Set keys = props.stringPropertyNames(); + List propertyEntries = new ArrayList<>(); - if (ctry.length() != 2) { - // ignore invalid country code - info("currency.properties entry for " + ctry + - " is ignored because of the invalid country code.", null); - return; - } + // remove all invalid entries and parse all valid currency properties + // entries to a group of CurrencyProperty, classified by currency code + Map> currencyCodeGroup = keys.stream() + .map(k -> CurrencyProperty + .getValidEntry(k.toUpperCase(Locale.ROOT), + props.getProperty(k).toUpperCase(Locale.ROOT), + pattern)).flatMap(o -> o.stream()) + .collect(Collectors.groupingBy(entry -> entry.currencyCode)); - Matcher m = pattern.matcher(curdata); - if (!m.find() || (m.group(4) == null && countOccurrences(curdata, ',') >= 3)) { - // format is not recognized. ignore the data - // if group(4) date string is null and we've 4 values, bad date value - info("currency.properties entry for " + ctry + - " ignored because the value format is not recognized.", null); - return; - } - - try { - if (m.group(4) != null && !isPastCutoverDate(m.group(4))) { - info("currency.properties entry for " + ctry + - " ignored since cutover date has not passed :" + curdata, null); - return; + // check each group for inconsistencies + currencyCodeGroup.forEach((curCode, list) -> { + boolean inconsistent = CurrencyProperty + .containsInconsistentInstances(list); + if (inconsistent) { + list.forEach(prop -> CurrencyProperty.info("The property" + + " entry for " + prop.country + " is inconsistent." + + " Ignored.", null)); + } else { + propertyEntries.addAll(list); } - } catch (ParseException ex) { - info("currency.properties entry for " + ctry + - " ignored since exception encountered :" + ex.getMessage(), null); - return; - } + }); + + return propertyEntries; + } - String code = m.group(1); - int numeric = Integer.parseInt(m.group(2)); + /** + * Replaces currency data found in the properties file that + * java.util.currency.data designates. This method is invoked for + * each valid currency entry. + * + * @param prop CurrencyProperty instance of the valid property entry + */ + private static void replaceCurrencyData(CurrencyProperty prop) { + + + String ctry = prop.country; + String code = prop.currencyCode; + int numeric = prop.numericCode; + int fraction = prop.fraction; int entry = numeric << NUMERIC_CODE_SHIFT; - int fraction = Integer.parseInt(m.group(3)); - if (fraction > SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS) { - info("currency.properties entry for " + ctry + - " ignored since the fraction is more than " + - SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS + ":" + curdata, null); - return; - } int index = SpecialCaseEntry.indexOf(code, fraction, numeric); - /* if a country switches from simple case to special case or + + // If a new entry changes the numeric code/dfd of an existing + // currency code, update it in the sc list at the respective + // index and also change it in the other currencies list and + // main table (if that currency code is also used as a + // simple case). + + // If all three components do not match with the new entry, + // but the currency code exists in the special case list + // update the sc entry with the new entry + int scCurrencyCodeIndex = -1; + if (index == -1) { + scCurrencyCodeIndex = SpecialCaseEntry.currencyCodeIndex(code); + if (scCurrencyCodeIndex != -1) { + //currency code exists in sc list, then update the old entry + specialCasesList.set(scCurrencyCodeIndex, + new SpecialCaseEntry(code, fraction, numeric)); + + // also update the entry in other currencies list + OtherCurrencyEntry oe = OtherCurrencyEntry.findEntry(code); + if (oe != null) { + int oIndex = otherCurrenciesList.indexOf(oe); + otherCurrenciesList.set(oIndex, new OtherCurrencyEntry( + code, fraction, numeric)); + } + } + } + + /* If a country switches from simple case to special case or * one special case to other special case which is not present - * in the sc arrays then insert the new entry in special case arrays + * in the sc arrays then insert the new entry in special case arrays. + * If an entry with given currency code exists, update with the new + * entry. */ if (index == -1 && (ctry.charAt(0) != code.charAt(0) || ctry.charAt(1) != code.charAt(1))) { - specialCasesList.add(new SpecialCaseEntry(code, fraction, numeric)); - index = specialCasesList.size() - 1; + if(scCurrencyCodeIndex == -1) { + specialCasesList.add(new SpecialCaseEntry(code, fraction, + numeric)); + index = specialCasesList.size() - 1; + } else { + index = scCurrencyCodeIndex; + } + + // update the entry in main table if it exists as a simple case + updateMainTableEntry(code, fraction, numeric); } if (index == -1) { @@ -848,32 +895,29 @@ setMainTableEntry(ctry.charAt(0), ctry.charAt(1), entry); } - private static boolean isPastCutoverDate(String s) throws ParseException { - SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ROOT); - format.setTimeZone(TimeZone.getTimeZone("UTC")); - format.setLenient(false); - long time = format.parse(s.trim()).getTime(); - return System.currentTimeMillis() > time; - - } + // update the entry in maintable for any simple case found, if a new + // entry as a special case updates the entry in sc list with + // existing currency code + private static void updateMainTableEntry(String code, int fraction, + int numeric) { + // checking the existence of currency code in mainTable + int tableEntry = getMainTableEntry(code.charAt(0), code.charAt(1)); + int entry = numeric << NUMERIC_CODE_SHIFT; + if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK + && tableEntry != INVALID_COUNTRY_ENTRY + && code.charAt(2) - 'A' == (tableEntry + & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK)) { - private static int countOccurrences(String value, char match) { - int count = 0; - for (char c : value.toCharArray()) { - if (c == match) { - ++count; - } - } - return count; - } - - private static void info(String message, Throwable t) { - PlatformLogger logger = PlatformLogger.getLogger("java.util.Currency"); - if (logger.isLoggable(PlatformLogger.Level.INFO)) { - if (t != null) { - logger.info(message, t); - } else { - logger.info(message); + int numericCode = (tableEntry & NUMERIC_CODE_MASK) + >> NUMERIC_CODE_SHIFT; + int defaultFractionDigits = (tableEntry + & SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK) + >> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT; + if (numeric != numericCode || fraction != defaultFractionDigits) { + // update the entry in main table + entry |= (fraction << SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT) + | (code.charAt(2) - 'A'); + setMainTableEntry(code.charAt(0), code.charAt(1), entry); } } } @@ -959,6 +1003,25 @@ return fractionAndNumericCode; } + // get the index based on currency code + private static int currencyCodeIndex(String code) { + int size = specialCasesList.size(); + for (int index = 0; index < size; index++) { + SpecialCaseEntry scEntry = specialCasesList.get(index); + if (scEntry.oldCurrency.equals(code) && (scEntry.cutOverTime == Long.MAX_VALUE + || System.currentTimeMillis() < scEntry.cutOverTime)) { + //consider only when there is no new currency or cutover time is not passed + return index; + } else if (scEntry.newCurrency.equals(code) + && System.currentTimeMillis() >= scEntry.cutOverTime) { + //consider only if the cutover time is passed + return index; + } + } + return -1; + } + + // convert the special case entry to sc arrays index private static int toIndex(int tableEntry) { return (tableEntry & SPECIAL_CASE_COUNTRY_INDEX_MASK) - SPECIAL_CASE_COUNTRY_INDEX_DELTA; @@ -999,6 +1062,136 @@ } + + /* + * Used to represent an entry of the properties file that + * java.util.currency.data designates + * + * - country: country representing the currency entry + * - currencyCode: currency code + * - fraction: default fraction digit + * - numericCode: numeric code + * - date: cutover date + */ + private static class CurrencyProperty { + final private String country; + final private String currencyCode; + final private int fraction; + final private int numericCode; + final private String date; + + private CurrencyProperty(String country, String currencyCode, + int fraction, int numericCode, String date) { + this.country = country; + this.currencyCode = currencyCode; + this.fraction = fraction; + this.numericCode = numericCode; + this.date = date; + } + + /** + * Check the valid currency data and create/return an Optional instance + * of CurrencyProperty + * + * @param ctry country representing the currency data + * @param curData currency data of the given {@code ctry} + * @param pattern regex pattern for the properties entry + * @return Optional containing CurrencyProperty instance, If valid; + * empty otherwise + */ + private static Optional getValidEntry(String ctry, + String curData, + Pattern pattern) { + + CurrencyProperty prop = null; + + if (ctry.length() != 2) { + // Invalid country code. Ignore the entry. + } else { + + prop = parseProperty(ctry, curData, pattern); + // if the property entry failed any of the below checked + // criteria it is ignored + if (prop == null + || (prop.date == null && curData.chars() + .map(c -> c == ',' ? 1 : 0).sum() >= 3)) { + // format is not recognized. ignore the data if date + // string is null and we've 4 values, bad date value + prop = null; + } else if (prop.fraction + > SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS) { + prop = null; + } else { + try { + if (prop.date != null + && !isPastCutoverDate(prop.date)) { + prop = null; + } + } catch (ParseException ex) { + prop = null; + } + } + } + + if (prop == null) { + info("The property entry for " + ctry + " is invalid." + + " Ignored.", null); + } + + return Optional.ofNullable(prop); + } + + /* + * Parse properties entry and return CurrencyProperty instance + */ + private static CurrencyProperty parseProperty(String ctry, + String curData, Pattern pattern) { + Matcher m = pattern.matcher(curData); + if (!m.find()) { + return null; + } else { + return new CurrencyProperty(ctry, m.group(1), + Integer.parseInt(m.group(3)), + Integer.parseInt(m.group(2)), m.group(4)); + } + } + + /** + * Checks if the given list contains multiple inconsistent currency instances + */ + private static boolean containsInconsistentInstances( + List list) { + int numCode = list.get(0).numericCode; + int fractionDigit = list.get(0).fraction; + return list.stream().anyMatch(prop -> prop.numericCode != numCode + || prop.fraction != fractionDigit); + } + + private static boolean isPastCutoverDate(String s) + throws ParseException { + SimpleDateFormat format = new SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss", Locale.ROOT); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + format.setLenient(false); + long time = format.parse(s.trim()).getTime(); + return System.currentTimeMillis() > time; + + } + + private static void info(String message, Throwable t) { + PlatformLogger logger = PlatformLogger + .getLogger("java.util.Currency"); + if (logger.isLoggable(PlatformLogger.Level.INFO)) { + if (t != null) { + logger.info(message, t); + } else { + logger.info(message); + } + } + } + + } + } diff -r cc30928a834e -r 28d8fc8cd3cd test/jdk/java/util/Currency/PropertiesTest.java --- a/test/jdk/java/util/Currency/PropertiesTest.java Wed Feb 21 15:09:40 2018 -0800 +++ b/test/jdk/java/util/Currency/PropertiesTest.java Thu Feb 22 11:52:01 2018 +0530 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007, 2016, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2007, 2018, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -37,6 +37,8 @@ bug7102969(); } else if (args.length == 1 && args[0].equals("bug8157138")) { bug8157138(); + } else if (args.length == 1 && args[0].equals("bug8190904")) { + bug8190904(); } else { System.err.println("Usage: java PropertiesTest -d "); System.err.println(" java PropertiesTest -c "); @@ -118,8 +120,9 @@ for (String key: keys) { String val = p.getProperty(key); try { - if (countOccurrences(val, ',') == 3 && !isPastCutoverDate(val)) { - System.out.println("Skipping since date is in future"); + if (val.chars().map(c -> c == ',' ? 1 : 0).sum() >= 3 + && !isPastCutoverDate(val)) { + System.out.println("Skipping " + key + " since date is in future"); continue; // skip since date in future (no effect) } } catch (ParseException pe) { @@ -130,6 +133,13 @@ System.out.printf("Testing key: %s, val: %s... ", key, val); System.out.println("AfterVal is : " + afterVal); + if (afterVal == null) { + System.out.println("Testing key " + key + " is ignored" + + " because of the inconsistent numeric code and/or" + + " dfd for the given currency code: "+val); + continue; + } + Matcher m = propertiesPattern.matcher(val.toUpperCase(Locale.ROOT)); if (!m.find()) { // format is not recognized. @@ -166,22 +176,24 @@ System.out.printf("Success!\n"); } if (!after.isEmpty()) { - StringBuilder sb = new StringBuilder() - .append("Currency data replacement failed. Unnecessary modification was(were) made for the following currencies:\n"); + keys = after.stringPropertyNames(); for (String key : keys) { - sb.append(" country: ") - .append(key) - .append(" currency: ") - .append(after.getProperty(key)) - .append("\n"); + String modified = after.getProperty(key); + if(!p.containsValue(modified)) { + throw new RuntimeException("Unnecessary modification was" + + " made to county: "+ key + " with currency value:" + + " " + modified); + } else { + System.out.println(key + " modified by an entry in" + + " currency.properties with currency value " + + modified); + } } - throw new RuntimeException(sb.toString()); } } private static void bug7102969() { - // check the correct overriding of special case entries Currency cur = Currency.getInstance(new Locale("", "JP")); if (!cur.getCurrencyCode().equals("ABC")) { @@ -248,6 +260,41 @@ } + private static void bug8190904() { + // should throw IllegalArgumentException as currency code + // does not exist as valid ISO 4217 code and failed to load + // from currency.properties file because of inconsistent numeric/dfd + try { + Currency.getInstance("MCC"); + throw new RuntimeException("[FAILED: Should throw" + + " IllegalArgumentException for invalid currency code]"); + } catch (IllegalArgumentException ex) { + // expected to throw IllegalArgumentException + } + + // should keep the XOF instance as XOF,952,0, as the XOF entries in + // currency.properties IT=XOF,952,1, XY=XOF,955,0 are ignored because + // of inconsistency in numeric code and/or dfd + checkCurrencyInstance("XOF", 952, 0); + // property entry "AS=USD,841,2" should change all occurences + // of USD with USD,841,2 + checkCurrencyInstance("USD", 841, 2); + } + + /** + * Test the numeric code and fraction of the Currency instance obtained + * by given currencyCode, with the expected numericCode and fraction + */ + private static void checkCurrencyInstance(String currencyCode, + int numericCode, int fraction) { + Currency cur = Currency.getInstance(currencyCode); + if (cur.getNumericCode() != numericCode + || cur.getDefaultFractionDigits() != fraction) { + throw new RuntimeException("[FAILED: Incorrect numeric code or" + + " dfd for currency code: " + currencyCode + "]"); + } + } + private static boolean isPastCutoverDate(String s) throws IndexOutOfBoundsException, NullPointerException, ParseException { String dateString = s.substring(s.lastIndexOf(',')+1, s.length()).trim(); @@ -263,13 +310,4 @@ } } - private static int countOccurrences(String value, char match) { - int count = 0; - for (char c : value.toCharArray()) { - if (c == match) { - ++count; - } - } - return count; - } } diff -r cc30928a834e -r 28d8fc8cd3cd test/jdk/java/util/Currency/PropertiesTest.sh --- a/test/jdk/java/util/Currency/PropertiesTest.sh Wed Feb 21 15:09:40 2018 -0800 +++ b/test/jdk/java/util/Currency/PropertiesTest.sh Thu Feb 22 11:52:01 2018 +0530 @@ -1,6 +1,6 @@ #!/bin/sh -# Copyright (c) 2007, 2016, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2007, 2018, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # This code is free software; you can redistribute it and/or modify it @@ -24,7 +24,7 @@ # @test # @bug 6332666 6863624 7180362 8003846 8074350 8074351 8130246 8149735 7102969 -# 8157138 +# 8157138 8190904 # @summary tests the capability of replacing the currency data with user # specified currency properties file # @build PropertiesTest @@ -124,6 +124,11 @@ ${WRITABLEJDK}${FS}bin${FS}java ${TESTVMOPTS} -cp ${TESTCLASSES} PropertiesTest bug8157138 if [ $? != 0 ]; then failures=`expr $failures + 1`; fi +# run bug8190904 test +echo '' +${WRITABLEJDK}${FS}bin${FS}java ${TESTVMOPTS} -cp ${TESTCLASSES} PropertiesTest bug8190904 +if [ $? != 0 ]; then failures=`expr $failures + 1`; fi + # Cleanup rm -rf $WRITABLEJDK diff -r cc30928a834e -r 28d8fc8cd3cd test/jdk/java/util/Currency/currency.properties --- a/test/jdk/java/util/Currency/currency.properties Wed Feb 21 15:09:40 2018 -0800 +++ b/test/jdk/java/util/Currency/currency.properties Thu Feb 22 11:52:01 2018 +0530 @@ -26,5 +26,9 @@ MG=MGG,990,10 MX=SSS,493,2,2001-01-01-00-00-00 PE=EUR ,978 ,2, 20399-01-01T00:00:00 -MG=MGG,990,10 =euR,111,2, 2099-01-01-00-00-00 +MR=MCC,556,7 +IT=XOF,952,1 +XY=XOF,955,0 +AS=USD,841,2 +CY=CYP,822,2