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 }