/* * Copyright (c) 2001, 2016, 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 * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package build.tools.generatecurrencydata; import java.io.IOException; import java.io.FileNotFoundException; import java.io.DataOutputStream; import java.io.FileOutputStream; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Locale; import java.util.Objects; import java.util.Properties; import java.util.TimeZone; /** * Reads currency data in properties format from the file specified in the * command line and generates a binary data file as specified in the command line. * * Output of this tool is a binary file that contains the data in * the following order: * * - magic number (int): always 0x43757244 ('CurD') * - formatVersion (int) * - dataVersion (int) * - mainTable (int[26*26]) * - specialCaseCount (int) * - specialCaseCutOverTimes (long[specialCaseCount]) * - specialCaseOldCurrencies (String[specialCaseCount]) * - specialCaseNewCurrencies (String[specialCaseCount]) * - specialCaseOldCurrenciesDefaultFractionDigits (int[specialCaseCount]) * - specialCaseNewCurrenciesDefaultFractionDigits (int[specialCaseCount]) * - specialCaseOldCurrenciesNumericCode (int[specialCaseCount]) * - specialCaseNewCurrenciesNumericCode (int[specialCaseCount]) * - otherCurrenciesCount (int) * - otherCurrencies (String) * - otherCurrenciesDefaultFractionDigits (int[otherCurrenciesCount]) * - otherCurrenciesNumericCode (int[otherCurrenciesCount]) * * See CurrencyData.properties for the input format description and * Currency.java for the format descriptions of the generated tables. */ public class GenerateCurrencyData { private static DataOutputStream out; // input data: currency data obtained from properties on input stream private static Properties currencyData; private static String formatVersion; private static String dataVersion; private static String validCurrencyCodes; // handy constants - must match definitions in java.util.Currency // magic number private static final int MAGIC_NUMBER = 0x43757244; // number of characters from A to Z private static final int A_TO_Z = ('Z' - 'A') + 1; // entry for invalid country codes private static final int INVALID_COUNTRY_ENTRY = 0x0000007F; // entry for countries without currency private static final int COUNTRY_WITHOUT_CURRENCY_ENTRY = 0x00000200; // mask for simple case country entries private static final int SIMPLE_CASE_COUNTRY_MASK = 0x00000000; // mask for simple case country entry final character private static final int SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK = 0x0000001F; // mask for simple case country entry default currency digits private static final int SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK = 0x000001E0; // shift count for simple case country entry default currency digits private static final int SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT = 5; // maximum number for simple case country entry default currency digits private static final int SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS = 9; // mask for special case country entries private static final int SPECIAL_CASE_COUNTRY_MASK = 0x00000200; // mask for special case country index private static final int SPECIAL_CASE_COUNTRY_INDEX_MASK = 0x0000001F; // delta from entry index component in main table to index into special case tables private static final int SPECIAL_CASE_COUNTRY_INDEX_DELTA = 1; // mask for distinguishing simple and special case countries private static final int COUNTRY_TYPE_MASK = SIMPLE_CASE_COUNTRY_MASK | SPECIAL_CASE_COUNTRY_MASK; // mask for the numeric code of the currency private static final int NUMERIC_CODE_MASK = 0x000FFC00; // shift count for the numeric code of the currency private static final int NUMERIC_CODE_SHIFT = 10; // generated data private static int[] mainTable = new int[A_TO_Z * A_TO_Z]; private static final int maxSpecialCases = 30; private static int specialCaseCount = 0; private static long[] specialCaseCutOverTimes = new long[maxSpecialCases]; private static String[] specialCaseOldCurrencies = new String[maxSpecialCases]; private static String[] specialCaseNewCurrencies = new String[maxSpecialCases]; private static int[] specialCaseOldCurrenciesDefaultFractionDigits = new int[maxSpecialCases]; private static int[] specialCaseNewCurrenciesDefaultFractionDigits = new int[maxSpecialCases]; private static int[] specialCaseOldCurrenciesNumericCode = new int[maxSpecialCases]; private static int[] specialCaseNewCurrenciesNumericCode = new int[maxSpecialCases]; private static final int maxOtherCurrencies = 128; private static int otherCurrenciesCount = 0; private static String[] otherCurrencies = new String[maxOtherCurrencies]; private static int[] otherCurrenciesDefaultFractionDigits = new int[maxOtherCurrencies]; private static int[] otherCurrenciesNumericCode= new int[maxOtherCurrencies]; // date format for parsing cut-over times private static SimpleDateFormat format; // Minor Units private static String[] currenciesWithDefinedMinorUnitDecimals = new String[SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS + 1]; private static String currenciesWithMinorUnitsUndefined; public static void main(String[] args) { // Look for "-o outputfilename" option if ( args.length == 2 && args[0].equals("-o") ) { try { out = new DataOutputStream(new FileOutputStream(args[1])); } catch ( FileNotFoundException e ) { System.err.println("Error: " + e.getMessage()); e.printStackTrace(System.err); System.exit(1); } } else { System.err.println("Error: Illegal arg count"); System.exit(1); } format = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US); format.setTimeZone(TimeZone.getTimeZone("GMT")); format.setLenient(false); try { readInput(); buildMainAndSpecialCaseTables(); buildOtherTables(); writeOutput(); out.flush(); out.close(); } catch (Exception e) { System.err.println("Error: " + e.getMessage()); e.printStackTrace(System.err); System.exit(1); } } private static void readInput() throws IOException { currencyData = new Properties(); currencyData.load(System.in); // initialize other lookup strings formatVersion = (String) currencyData.get("formatVersion"); dataVersion = (String) currencyData.get("dataVersion"); validCurrencyCodes = (String) currencyData.get("all"); for (int i = 0; i <= SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS; i++) { currenciesWithDefinedMinorUnitDecimals[i] = (String) currencyData.get("minor"+i); } currenciesWithMinorUnitsUndefined = (String) currencyData.get("minorUndefined"); if (formatVersion == null || dataVersion == null || validCurrencyCodes == null || currenciesWithMinorUnitsUndefined == null) { throw new NullPointerException("not all required data is defined in input"); } } private static void buildMainAndSpecialCaseTables() throws Exception { for (int first = 0; first < A_TO_Z; first++) { for (int second = 0; second < A_TO_Z; second++) { char firstChar = (char) ('A' + first); char secondChar = (char) ('A' + second); String countryCode = (new StringBuffer()).append(firstChar).append(secondChar).toString(); String currencyInfo = (String) currencyData.get(countryCode); int tableEntry = 0; if (currencyInfo == null) { // no entry -> must be invalid ISO 3166 country code tableEntry = INVALID_COUNTRY_ENTRY; } else { int length = currencyInfo.length(); if (length == 0) { // special case: country without currency tableEntry = COUNTRY_WITHOUT_CURRENCY_ENTRY; } else if (length == 3) { // valid currency if (currencyInfo.charAt(0) == firstChar && currencyInfo.charAt(1) == secondChar) { checkCurrencyCode(currencyInfo); int digits = getDefaultFractionDigits(currencyInfo); if (digits < 0 || digits > SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS) { throw new RuntimeException("fraction digits out of range for " + currencyInfo); } int numericCode= getNumericCode(currencyInfo); if (numericCode < 0 || numericCode >= 1000 ) { throw new RuntimeException("numeric code out of range for " + currencyInfo); } tableEntry = SIMPLE_CASE_COUNTRY_MASK | (currencyInfo.charAt(2) - 'A') | (digits << SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT) | (numericCode << NUMERIC_CODE_SHIFT); } else { tableEntry = SPECIAL_CASE_COUNTRY_MASK | (makeSpecialCaseEntry(currencyInfo) + SPECIAL_CASE_COUNTRY_INDEX_DELTA); } } else { tableEntry = SPECIAL_CASE_COUNTRY_MASK | (makeSpecialCaseEntry(currencyInfo) + SPECIAL_CASE_COUNTRY_INDEX_DELTA); } } mainTable[first * A_TO_Z + second] = tableEntry; } } } private static int getDefaultFractionDigits(String currencyCode) { for (int i = 0; i <= SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS; i++) { if (Objects.nonNull(currenciesWithDefinedMinorUnitDecimals[i]) && currenciesWithDefinedMinorUnitDecimals[i].indexOf(currencyCode) != -1) { return i; } } if (currenciesWithMinorUnitsUndefined.indexOf(currencyCode) != -1) { return -1; } else { return 2; } } private static int getNumericCode(String currencyCode) { int index = validCurrencyCodes.indexOf(currencyCode); String numericCode = validCurrencyCodes.substring(index + 3, index + 6); return Integer.parseInt(numericCode); } static HashMap<String, Integer> specialCaseMap = new HashMap<>(); private static int makeSpecialCaseEntry(String currencyInfo) throws Exception { Integer oldEntry = specialCaseMap.get(currencyInfo); if (oldEntry != null) { return oldEntry.intValue(); } if (specialCaseCount == maxSpecialCases) { throw new RuntimeException("too many special cases"); } if (currencyInfo.length() == 3) { checkCurrencyCode(currencyInfo); specialCaseCutOverTimes[specialCaseCount] = Long.MAX_VALUE; specialCaseOldCurrencies[specialCaseCount] = currencyInfo; specialCaseOldCurrenciesDefaultFractionDigits[specialCaseCount] = getDefaultFractionDigits(currencyInfo); specialCaseOldCurrenciesNumericCode[specialCaseCount] = getNumericCode(currencyInfo); specialCaseNewCurrencies[specialCaseCount] = null; specialCaseNewCurrenciesDefaultFractionDigits[specialCaseCount] = 0; specialCaseNewCurrenciesNumericCode[specialCaseCount] = 0; } else { int length = currencyInfo.length(); if (currencyInfo.charAt(3) != ';' || currencyInfo.charAt(length - 4) != ';') { throw new RuntimeException("invalid currency info: " + currencyInfo); } String oldCurrency = currencyInfo.substring(0, 3); String newCurrency = currencyInfo.substring(length - 3, length); checkCurrencyCode(oldCurrency); checkCurrencyCode(newCurrency); String timeString = currencyInfo.substring(4, length - 4); long time = format.parse(timeString).getTime(); if (Math.abs(time - System.currentTimeMillis()) > ((long) 10) * 365 * 24 * 60 * 60 * 1000) { throw new RuntimeException("time is more than 10 years from present: " + time); } specialCaseCutOverTimes[specialCaseCount] = time; specialCaseOldCurrencies[specialCaseCount] = oldCurrency; specialCaseOldCurrenciesDefaultFractionDigits[specialCaseCount] = getDefaultFractionDigits(oldCurrency); specialCaseOldCurrenciesNumericCode[specialCaseCount] = getNumericCode(oldCurrency); specialCaseNewCurrencies[specialCaseCount] = newCurrency; specialCaseNewCurrenciesDefaultFractionDigits[specialCaseCount] = getDefaultFractionDigits(newCurrency); specialCaseNewCurrenciesNumericCode[specialCaseCount] = getNumericCode(newCurrency); } specialCaseMap.put(currencyInfo, new Integer(specialCaseCount)); return specialCaseCount++; } private static void buildOtherTables() { if (validCurrencyCodes.length() % 7 != 6) { throw new RuntimeException("\"all\" entry has incorrect size"); } for (int i = 0; i < (validCurrencyCodes.length() + 1) / 7; i++) { if (i > 0 && validCurrencyCodes.charAt(i * 7 - 1) != '-') { throw new RuntimeException("incorrect separator in \"all\" entry"); } String currencyCode = validCurrencyCodes.substring(i * 7, i * 7 + 3); int numericCode = Integer.parseInt( validCurrencyCodes.substring(i * 7 + 3, i * 7 + 6)); checkCurrencyCode(currencyCode); int tableEntry = mainTable[(currencyCode.charAt(0) - 'A') * A_TO_Z + (currencyCode.charAt(1) - 'A')]; if (tableEntry == INVALID_COUNTRY_ENTRY || (tableEntry & SPECIAL_CASE_COUNTRY_MASK) != 0 || (tableEntry & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK) != (currencyCode.charAt(2) - 'A')) { if (otherCurrenciesCount == maxOtherCurrencies) { throw new RuntimeException("too many other currencies"); } otherCurrencies[otherCurrenciesCount] = currencyCode; otherCurrenciesDefaultFractionDigits[otherCurrenciesCount] = getDefaultFractionDigits(currencyCode); otherCurrenciesNumericCode[otherCurrenciesCount] = getNumericCode(currencyCode); otherCurrenciesCount++; } } } private static void checkCurrencyCode(String currencyCode) { if (currencyCode.length() != 3) { throw new RuntimeException("illegal length for currency code: " + currencyCode); } for (int i = 0; i < 3; i++) { char aChar = currencyCode.charAt(i); if ((aChar < 'A' || aChar > 'Z') && !currencyCode.equals("XB5")) { throw new RuntimeException("currency code contains illegal character: " + currencyCode); } } if (validCurrencyCodes.indexOf(currencyCode) == -1) { throw new RuntimeException("currency code not listed as valid: " + currencyCode); } } private static void writeOutput() throws IOException { out.writeInt(MAGIC_NUMBER); out.writeInt(Integer.parseInt(formatVersion)); out.writeInt(Integer.parseInt(dataVersion)); writeIntArray(mainTable, mainTable.length); out.writeInt(specialCaseCount); writeSpecialCaseEntries(); out.writeInt(otherCurrenciesCount); writeOtherCurrencies(); } private static void writeIntArray(int[] ia, int count) throws IOException { for (int i = 0; i < count; i++) { out.writeInt(ia[i]); } } private static void writeSpecialCaseEntries() throws IOException { for (int index = 0; index < specialCaseCount; index++) { out.writeLong(specialCaseCutOverTimes[index]); String str = (specialCaseOldCurrencies[index] != null) ? specialCaseOldCurrencies[index] : ""; out.writeUTF(str); str = (specialCaseNewCurrencies[index] != null) ? specialCaseNewCurrencies[index] : ""; out.writeUTF(str); out.writeInt(specialCaseOldCurrenciesDefaultFractionDigits[index]); out.writeInt(specialCaseNewCurrenciesDefaultFractionDigits[index]); out.writeInt(specialCaseOldCurrenciesNumericCode[index]); out.writeInt(specialCaseNewCurrenciesNumericCode[index]); } } private static void writeOtherCurrencies() throws IOException { for (int index = 0; index < otherCurrenciesCount; index++) { String str = (otherCurrencies[index] != null) ? otherCurrencies[index] : ""; out.writeUTF(str); out.writeInt(otherCurrenciesDefaultFractionDigits[index]); out.writeInt(otherCurrenciesNumericCode[index]); } } }