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