1 /*
   2  * Copyright (c) 2001, 2020, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   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
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package build.tools.generatecurrencydata;
  27 
  28 import java.io.IOException;
  29 import java.io.FileNotFoundException;
  30 import java.io.DataOutputStream;
  31 import java.io.FileInputStream;
  32 import java.io.FileOutputStream;
  33 import java.io.InputStream;
  34 import java.text.SimpleDateFormat;
  35 import java.util.Date;
  36 import java.util.HashMap;
  37 import java.util.Locale;
  38 import java.util.Objects;
  39 import java.util.Properties;
  40 import java.util.TimeZone;
  41 
  42 /**
  43  * Reads currency data in properties format from the file specified in the
  44  * command line and generates a binary data file as specified in the command line.
  45  *
  46  * Output of this tool is a binary file that contains the data in
  47  * the following order:
  48  *
  49  *     - magic number (int): always 0x43757244 ('CurD')
  50  *     - formatVersion (int)
  51  *     - dataVersion (int)
  52  *     - mainTable (int[26*26])
  53  *     - specialCaseCount (int)
  54  *     - specialCaseCutOverTimes (long[specialCaseCount])
  55  *     - specialCaseOldCurrencies (String[specialCaseCount])
  56  *     - specialCaseNewCurrencies (String[specialCaseCount])
  57  *     - specialCaseOldCurrenciesDefaultFractionDigits (int[specialCaseCount])
  58  *     - specialCaseNewCurrenciesDefaultFractionDigits (int[specialCaseCount])
  59  *     - specialCaseOldCurrenciesNumericCode (int[specialCaseCount])
  60  *     - specialCaseNewCurrenciesNumericCode (int[specialCaseCount])
  61  *     - otherCurrenciesCount (int)
  62  *     - otherCurrencies (String)
  63  *     - otherCurrenciesDefaultFractionDigits (int[otherCurrenciesCount])
  64  *     - otherCurrenciesNumericCode (int[otherCurrenciesCount])
  65  *
  66  * See CurrencyData.properties for the input format description and
  67  * Currency.java for the format descriptions of the generated tables.
  68  */
  69 public class GenerateCurrencyData {
  70 
  71     private static DataOutputStream out;
  72 
  73     // input data: currency data obtained from properties on input stream
  74     private static Properties currencyData;
  75     private static String formatVersion;
  76     private static String dataVersion;
  77     private static String validCurrencyCodes;
  78 
  79     // handy constants - must match definitions in java.util.Currency
  80     // magic number
  81     private static final int MAGIC_NUMBER = 0x43757244;
  82     // number of characters from A to Z
  83     private static final int A_TO_Z = ('Z' - 'A') + 1;
  84     // entry for invalid country codes
  85     private static final int INVALID_COUNTRY_ENTRY = 0x0000007F;
  86     // entry for countries without currency
  87     private static final int COUNTRY_WITHOUT_CURRENCY_ENTRY = 0x00000200;
  88     // mask for simple case country entries
  89     private static final int SIMPLE_CASE_COUNTRY_MASK = 0x00000000;
  90     // mask for simple case country entry final character
  91     private static final int SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK = 0x0000001F;
  92     // mask for simple case country entry default currency digits
  93     private static final int SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK = 0x000001E0;
  94     // shift count for simple case country entry default currency digits
  95     private static final int SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT = 5;
  96     // maximum number for simple case country entry default currency digits
  97     private static final int SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS = 9;
  98     // mask for special case country entries
  99     private static final int SPECIAL_CASE_COUNTRY_MASK = 0x00000200;
 100     // mask for special case country index
 101     private static final int SPECIAL_CASE_COUNTRY_INDEX_MASK = 0x0000001F;
 102     // delta from entry index component in main table to index into special case tables
 103     private static final int SPECIAL_CASE_COUNTRY_INDEX_DELTA = 1;
 104     // mask for distinguishing simple and special case countries
 105     private static final int COUNTRY_TYPE_MASK = SIMPLE_CASE_COUNTRY_MASK | SPECIAL_CASE_COUNTRY_MASK;
 106     // mask for the numeric code of the currency
 107     private static final int NUMERIC_CODE_MASK = 0x000FFC00;
 108     // shift count for the numeric code of the currency
 109     private static final int NUMERIC_CODE_SHIFT = 10;
 110 
 111     // generated data
 112     private static int[] mainTable = new int[A_TO_Z * A_TO_Z];
 113 
 114     private static final int maxSpecialCases = 30;
 115     private static int specialCaseCount = 0;
 116     private static long[] specialCaseCutOverTimes = new long[maxSpecialCases];
 117     private static String[] specialCaseOldCurrencies = new String[maxSpecialCases];
 118     private static String[] specialCaseNewCurrencies = new String[maxSpecialCases];
 119     private static int[] specialCaseOldCurrenciesDefaultFractionDigits = new int[maxSpecialCases];
 120     private static int[] specialCaseNewCurrenciesDefaultFractionDigits = new int[maxSpecialCases];
 121     private static int[] specialCaseOldCurrenciesNumericCode = new int[maxSpecialCases];
 122     private static int[] specialCaseNewCurrenciesNumericCode = new int[maxSpecialCases];
 123 
 124     private static final int maxOtherCurrencies = 128;
 125     private static int otherCurrenciesCount = 0;
 126     private static String[] otherCurrencies = new String[maxOtherCurrencies];
 127     private static int[] otherCurrenciesDefaultFractionDigits = new int[maxOtherCurrencies];
 128     private static int[] otherCurrenciesNumericCode= new int[maxOtherCurrencies];
 129 
 130     // date format for parsing cut-over times
 131     private static SimpleDateFormat format;
 132 
 133     // Minor Units
 134     private static String[] currenciesWithDefinedMinorUnitDecimals =
 135         new String[SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS + 1];
 136     private static String currenciesWithMinorUnitsUndefined;
 137 
 138     public static void main(String[] args) {
 139         InputStream in = System.in;
 140         // Look for "-o outputfilename" option
 141         for (int n = 0; n < args.length; ++n) {
 142             if (args[n].equals("-o")) {
 143                 ++n;
 144                 if (n >= args.length) {
 145                     System.err.println("Error: Invalid argument format");
 146                     System.exit(1);
 147                 }
 148                 try {
 149                     out = new DataOutputStream(new FileOutputStream(args[n]));
 150                 } catch ( FileNotFoundException e ) {
 151                     System.err.println("Error: " + e.getMessage());
 152                     e.printStackTrace(System.err);
 153                     System.exit(1);
 154                 }
 155             } else if (args[n].equals("-i")) {
 156                 ++n;
 157                 if (n >= args.length) {
 158                     System.err.println("Error: Invalid argument format");
 159                     System.exit(1);
 160                 }
 161                 try {
 162                     in = new FileInputStream(args[n]);
 163                 } catch ( FileNotFoundException e ) {
 164                     System.err.println("Error: " + e.getMessage());
 165                     e.printStackTrace(System.err);
 166                     System.exit(1);
 167                 }
 168             } else {
 169                 System.err.println("Error: Invalid argument " + args[n]);
 170                 System.exit(1);
 171             }
 172         }
 173 
 174         if (out == null) {
 175             System.err.println("Error: Invalid argument format");
 176             System.exit(1);
 177         }
 178 
 179         format = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US);
 180         format.setTimeZone(TimeZone.getTimeZone("GMT"));
 181         format.setLenient(false);
 182 
 183         try {
 184             readInput(in);
 185             buildMainAndSpecialCaseTables();
 186             buildOtherTables();
 187             writeOutput();
 188             out.flush();
 189             out.close();
 190         } catch (Exception e) {
 191             System.err.println("Error: " + e.getMessage());
 192             e.printStackTrace(System.err);
 193             System.exit(1);
 194         }
 195     }
 196 
 197     private static void readInput(InputStream in) throws IOException {
 198         currencyData = new Properties();
 199         currencyData.load(in);
 200 
 201         // initialize other lookup strings
 202         formatVersion = (String) currencyData.get("formatVersion");
 203         dataVersion = (String) currencyData.get("dataVersion");
 204         validCurrencyCodes = (String) currencyData.get("all");
 205         for (int i = 0; i <= SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS; i++) {
 206             currenciesWithDefinedMinorUnitDecimals[i]
 207                 = (String) currencyData.get("minor"+i);
 208         }
 209         currenciesWithMinorUnitsUndefined  = (String) currencyData.get("minorUndefined");
 210         if (formatVersion == null ||
 211                 dataVersion == null ||
 212                 validCurrencyCodes == null ||
 213                 currenciesWithMinorUnitsUndefined == null) {
 214             throw new NullPointerException("not all required data is defined in input");
 215         }
 216     }
 217 
 218     private static void buildMainAndSpecialCaseTables() throws Exception {
 219         for (int first = 0; first < A_TO_Z; first++) {
 220             for (int second = 0; second < A_TO_Z; second++) {
 221                 char firstChar = (char) ('A' + first);
 222                 char secondChar = (char) ('A' + second);
 223                 String countryCode = (new StringBuffer()).append(firstChar).append(secondChar).toString();
 224                 String currencyInfo = (String) currencyData.get(countryCode);
 225                 int tableEntry = 0;
 226                 if (currencyInfo == null) {
 227                     // no entry -> must be invalid ISO 3166 country code
 228                     tableEntry = INVALID_COUNTRY_ENTRY;
 229                 } else {
 230                     int length = currencyInfo.length();
 231                     if (length == 0) {
 232                         // special case: country without currency
 233                        tableEntry = COUNTRY_WITHOUT_CURRENCY_ENTRY;
 234                     } else if (length == 3) {
 235                         // valid currency
 236                         if (currencyInfo.charAt(0) == firstChar && currencyInfo.charAt(1) == secondChar) {
 237                             checkCurrencyCode(currencyInfo);
 238                             int digits = getDefaultFractionDigits(currencyInfo);
 239                             if (digits < 0 || digits > SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS) {
 240                                 throw new RuntimeException("fraction digits out of range for " + currencyInfo);
 241                             }
 242                             int numericCode= getNumericCode(currencyInfo);
 243                             if (numericCode < 0 || numericCode >= 1000 ) {
 244                                 throw new RuntimeException("numeric code out of range for " + currencyInfo);
 245                             }
 246                             tableEntry = SIMPLE_CASE_COUNTRY_MASK
 247                                     | (currencyInfo.charAt(2) - 'A')
 248                                     | (digits << SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT)
 249                                     | (numericCode << NUMERIC_CODE_SHIFT);
 250                         } else {
 251                             tableEntry = SPECIAL_CASE_COUNTRY_MASK | (makeSpecialCaseEntry(currencyInfo) + SPECIAL_CASE_COUNTRY_INDEX_DELTA);
 252                         }
 253                     } else {
 254                         tableEntry = SPECIAL_CASE_COUNTRY_MASK | (makeSpecialCaseEntry(currencyInfo) + SPECIAL_CASE_COUNTRY_INDEX_DELTA);
 255                     }
 256                 }
 257                 mainTable[first * A_TO_Z + second] = tableEntry;
 258             }
 259         }
 260     }
 261 
 262     private static int getDefaultFractionDigits(String currencyCode) {
 263         for (int i = 0; i <= SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS; i++) {
 264             if (Objects.nonNull(currenciesWithDefinedMinorUnitDecimals[i]) &&
 265                 currenciesWithDefinedMinorUnitDecimals[i].indexOf(currencyCode) != -1) {
 266                 return i;
 267             }
 268         }
 269 
 270         if (currenciesWithMinorUnitsUndefined.indexOf(currencyCode) != -1) {
 271             return -1;
 272         } else {
 273             return 2;
 274         }
 275     }
 276 
 277     private static int getNumericCode(String currencyCode) {
 278         int index = validCurrencyCodes.indexOf(currencyCode);
 279         String numericCode = validCurrencyCodes.substring(index + 3, index + 6);
 280         return Integer.parseInt(numericCode);
 281     }
 282 
 283     static HashMap<String, Integer> specialCaseMap = new HashMap<>();
 284 
 285     private static int makeSpecialCaseEntry(String currencyInfo) throws Exception {
 286         Integer oldEntry = specialCaseMap.get(currencyInfo);
 287         if (oldEntry != null) {
 288             return oldEntry.intValue();
 289         }
 290         if (specialCaseCount == maxSpecialCases) {
 291             throw new RuntimeException("too many special cases");
 292         }
 293         if (currencyInfo.length() == 3) {
 294             checkCurrencyCode(currencyInfo);
 295             specialCaseCutOverTimes[specialCaseCount] = Long.MAX_VALUE;
 296             specialCaseOldCurrencies[specialCaseCount] = currencyInfo;
 297             specialCaseOldCurrenciesDefaultFractionDigits[specialCaseCount] = getDefaultFractionDigits(currencyInfo);
 298             specialCaseOldCurrenciesNumericCode[specialCaseCount] = getNumericCode(currencyInfo);
 299             specialCaseNewCurrencies[specialCaseCount] = null;
 300             specialCaseNewCurrenciesDefaultFractionDigits[specialCaseCount] = 0;
 301             specialCaseNewCurrenciesNumericCode[specialCaseCount] = 0;
 302         } else {
 303             int length = currencyInfo.length();
 304             if (currencyInfo.charAt(3) != ';' ||
 305                     currencyInfo.charAt(length - 4) != ';') {
 306                 throw new RuntimeException("invalid currency info: " + currencyInfo);
 307             }
 308             String oldCurrency = currencyInfo.substring(0, 3);
 309             String newCurrency = currencyInfo.substring(length - 3, length);
 310             checkCurrencyCode(oldCurrency);
 311             checkCurrencyCode(newCurrency);
 312             String timeString = currencyInfo.substring(4, length - 4);
 313             long time = format.parse(timeString).getTime();
 314             if (Math.abs(time - System.currentTimeMillis()) > ((long) 10) * 365 * 24 * 60 * 60 * 1000) {
 315                 throw new RuntimeException("time is more than 10 years from present: " + time);
 316             }
 317             specialCaseCutOverTimes[specialCaseCount] = time;
 318             specialCaseOldCurrencies[specialCaseCount] = oldCurrency;
 319             specialCaseOldCurrenciesDefaultFractionDigits[specialCaseCount] = getDefaultFractionDigits(oldCurrency);
 320             specialCaseOldCurrenciesNumericCode[specialCaseCount] = getNumericCode(oldCurrency);
 321             specialCaseNewCurrencies[specialCaseCount] = newCurrency;
 322             specialCaseNewCurrenciesDefaultFractionDigits[specialCaseCount] = getDefaultFractionDigits(newCurrency);
 323             specialCaseNewCurrenciesNumericCode[specialCaseCount] = getNumericCode(newCurrency);
 324         }
 325         specialCaseMap.put(currencyInfo, Integer.valueOf(specialCaseCount));
 326         return specialCaseCount++;
 327     }
 328 
 329     private static void buildOtherTables() {
 330         if (validCurrencyCodes.length() % 7 != 6) {
 331             throw new RuntimeException("\"all\" entry has incorrect size");
 332         }
 333         for (int i = 0; i < (validCurrencyCodes.length() + 1) / 7; i++) {
 334             if (i > 0 && validCurrencyCodes.charAt(i * 7 - 1) != '-') {
 335                 throw new RuntimeException("incorrect separator in \"all\" entry");
 336             }
 337             String currencyCode = validCurrencyCodes.substring(i * 7, i * 7 + 3);
 338             int numericCode = Integer.parseInt(
 339                 validCurrencyCodes.substring(i * 7 + 3, i * 7 + 6));
 340             checkCurrencyCode(currencyCode);
 341             int tableEntry = mainTable[(currencyCode.charAt(0) - 'A') * A_TO_Z + (currencyCode.charAt(1) - 'A')];
 342             if (tableEntry == INVALID_COUNTRY_ENTRY ||
 343                     (tableEntry & SPECIAL_CASE_COUNTRY_MASK) != 0 ||
 344                     (tableEntry & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK) != (currencyCode.charAt(2) - 'A')) {
 345                 if (otherCurrenciesCount == maxOtherCurrencies) {
 346                     throw new RuntimeException("too many other currencies");
 347                 }
 348                 otherCurrencies[otherCurrenciesCount] = currencyCode;
 349                 otherCurrenciesDefaultFractionDigits[otherCurrenciesCount] = getDefaultFractionDigits(currencyCode);
 350                 otherCurrenciesNumericCode[otherCurrenciesCount] = getNumericCode(currencyCode);
 351                 otherCurrenciesCount++;
 352             }
 353         }
 354     }
 355 
 356     private static void checkCurrencyCode(String currencyCode) {
 357         if (currencyCode.length() != 3) {
 358             throw new RuntimeException("illegal length for currency code: " + currencyCode);
 359         }
 360         for (int i = 0; i < 3; i++) {
 361             char aChar = currencyCode.charAt(i);
 362             if ((aChar < 'A' || aChar > 'Z') && !currencyCode.equals("XB5")) {
 363                 throw new RuntimeException("currency code contains illegal character: " + currencyCode);
 364             }
 365         }
 366         if (validCurrencyCodes.indexOf(currencyCode) == -1) {
 367             throw new RuntimeException("currency code not listed as valid: " + currencyCode);
 368         }
 369     }
 370 
 371     private static void writeOutput() throws IOException {
 372         out.writeInt(MAGIC_NUMBER);
 373         out.writeInt(Integer.parseInt(formatVersion));
 374         out.writeInt(Integer.parseInt(dataVersion));
 375         writeIntArray(mainTable, mainTable.length);
 376         out.writeInt(specialCaseCount);
 377         writeSpecialCaseEntries();
 378         out.writeInt(otherCurrenciesCount);
 379         writeOtherCurrencies();
 380     }
 381 
 382     private static void writeIntArray(int[] ia, int count) throws IOException {
 383         for (int i = 0; i < count; i++) {
 384             out.writeInt(ia[i]);
 385         }
 386     }
 387 
 388     private static void writeSpecialCaseEntries() throws IOException {
 389         for (int index = 0; index < specialCaseCount; index++) {
 390             out.writeLong(specialCaseCutOverTimes[index]);
 391             String str = (specialCaseOldCurrencies[index] != null)
 392                     ? specialCaseOldCurrencies[index] : "";
 393             out.writeUTF(str);
 394             str = (specialCaseNewCurrencies[index] != null)
 395                     ? specialCaseNewCurrencies[index] : "";
 396             out.writeUTF(str);
 397             out.writeInt(specialCaseOldCurrenciesDefaultFractionDigits[index]);
 398             out.writeInt(specialCaseNewCurrenciesDefaultFractionDigits[index]);
 399             out.writeInt(specialCaseOldCurrenciesNumericCode[index]);
 400             out.writeInt(specialCaseNewCurrenciesNumericCode[index]);
 401         }
 402     }
 403 
 404     private static void writeOtherCurrencies() throws IOException {
 405         for (int index = 0; index < otherCurrenciesCount; index++) {
 406             String str = (otherCurrencies[index] != null)
 407                     ? otherCurrencies[index] : "";
 408             out.writeUTF(str);
 409             out.writeInt(otherCurrenciesDefaultFractionDigits[index]);
 410             out.writeInt(otherCurrenciesNumericCode[index]);
 411         }
 412     }
 413 
 414 }