1/*
2 * Copyright (c) 2001, 2016, 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
26package build.tools.generatecurrencydata;
27
28import java.io.IOException;
29import java.io.FileNotFoundException;
30import java.io.DataOutputStream;
31import java.io.FileOutputStream;
32import java.text.SimpleDateFormat;
33import java.util.Date;
34import java.util.HashMap;
35import java.util.Locale;
36import java.util.Objects;
37import java.util.Properties;
38import 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 */
67public 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 String[] otherCurrencies = new String[maxOtherCurrencies];
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                otherCurrencies[otherCurrenciesCount] = currencyCode;
322                otherCurrenciesDefaultFractionDigits[otherCurrenciesCount] = getDefaultFractionDigits(currencyCode);
323                otherCurrenciesNumericCode[otherCurrenciesCount] = getNumericCode(currencyCode);
324                otherCurrenciesCount++;
325            }
326        }
327    }
328
329    private static void checkCurrencyCode(String currencyCode) {
330        if (currencyCode.length() != 3) {
331            throw new RuntimeException("illegal length for currency code: " + currencyCode);
332        }
333        for (int i = 0; i < 3; i++) {
334            char aChar = currencyCode.charAt(i);
335            if ((aChar < 'A' || aChar > 'Z') && !currencyCode.equals("XB5")) {
336                throw new RuntimeException("currency code contains illegal character: " + currencyCode);
337            }
338        }
339        if (validCurrencyCodes.indexOf(currencyCode) == -1) {
340            throw new RuntimeException("currency code not listed as valid: " + currencyCode);
341        }
342    }
343
344    private static void writeOutput() throws IOException {
345        out.writeInt(MAGIC_NUMBER);
346        out.writeInt(Integer.parseInt(formatVersion));
347        out.writeInt(Integer.parseInt(dataVersion));
348        writeIntArray(mainTable, mainTable.length);
349        out.writeInt(specialCaseCount);
350        writeSpecialCaseEntries();
351        out.writeInt(otherCurrenciesCount);
352        writeOtherCurrencies();
353    }
354
355    private static void writeIntArray(int[] ia, int count) throws IOException {
356        for (int i = 0; i < count; i++) {
357            out.writeInt(ia[i]);
358        }
359    }
360
361    private static void writeSpecialCaseEntries() throws IOException {
362        for (int index = 0; index < specialCaseCount; index++) {
363            out.writeLong(specialCaseCutOverTimes[index]);
364            String str = (specialCaseOldCurrencies[index] != null)
365                    ? specialCaseOldCurrencies[index] : "";
366            out.writeUTF(str);
367            str = (specialCaseNewCurrencies[index] != null)
368                    ? specialCaseNewCurrencies[index] : "";
369            out.writeUTF(str);
370            out.writeInt(specialCaseOldCurrenciesDefaultFractionDigits[index]);
371            out.writeInt(specialCaseNewCurrenciesDefaultFractionDigits[index]);
372            out.writeInt(specialCaseOldCurrenciesNumericCode[index]);
373            out.writeInt(specialCaseNewCurrenciesNumericCode[index]);
374        }
375    }
376
377    private static void writeOtherCurrencies() throws IOException {
378        for (int index = 0; index < otherCurrenciesCount; index++) {
379            String str = (otherCurrencies[index] != null)
380                    ? otherCurrencies[index] : "";
381            out.writeUTF(str);
382            out.writeInt(otherCurrenciesDefaultFractionDigits[index]);
383            out.writeInt(otherCurrenciesNumericCode[index]);
384        }
385    }
386
387}
388