8190904: Incorrect currency instance returned by java.util.Currency.getInstance()

Reviewed-by: naoto
This commit is contained in:
Nishit Jain 2018-02-22 11:52:01 +05:30
parent eab5dab516
commit cff8ccbcf7
4 changed files with 350 additions and 110 deletions

View File

@ -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.concurrent.ConcurrentMap;
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 @@ import sun.util.logging.PlatformLogger;
* JP=JPZ,999,0
* </code>
* <p>
* 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.
*
* <p>
* <code>
@ -93,6 +97,11 @@ import sun.util.logging.PlatformLogger;
* 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.
* <p>
* 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.
*
* <p>
* 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 @@ public final class Currency implements Serializable {
try (FileReader fr = new FileReader(propFile)) {
props.load(fr);
}
Set<String> 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<CurrencyProperty> 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 @@ public final class Currency implements Serializable {
}
/**
* 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<CurrencyProperty> getValidCurrencyData(Properties props,
Pattern pattern) {
if (ctry.length() != 2) {
// ignore invalid country code
info("currency.properties entry for " + ctry +
" is ignored because of the invalid country code.", null);
return;
}
Set<String> keys = props.stringPropertyNames();
List<CurrencyProperty> propertyEntries = new ArrayList<>();
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;
}
// remove all invalid entries and parse all valid currency properties
// entries to a group of CurrencyProperty, classified by currency code
Map<String, List<CurrencyProperty>> 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));
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;
}
});
String code = m.group(1);
int numeric = Integer.parseInt(m.group(2));
return propertyEntries;
}
/**
* 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 @@ public final class Currency implements Serializable {
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 @@ public final class Currency implements Serializable {
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 @@ public final class Currency implements Serializable {
}
/*
* 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<CurrencyProperty> 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<CurrencyProperty> 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);
}
}
}
}
}

View File

@ -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 @@ public class PropertiesTest {
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 <dumpfile>");
System.err.println(" java PropertiesTest -c <beforedump> <afterdump> <propsfile>");
@ -118,8 +120,9 @@ public class PropertiesTest {
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 @@ public class PropertiesTest {
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 @@ public class PropertiesTest {
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 @@ public class PropertiesTest {
}
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 @@ public class PropertiesTest {
}
}
private static int countOccurrences(String value, char match) {
int count = 0;
for (char c : value.toCharArray()) {
if (c == match) {
++count;
}
}
return count;
}
}

View File

@ -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 @@ echo ''
${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

View File

@ -26,5 +26,9 @@ IE=euR,111,2,#testcomment
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