1/*
2 *******************************************************************************
3 * Copyright (C) 2014, International Business Machines Corporation and
4 * others. All Rights Reserved.
5 *******************************************************************************
6 */
7package com.ibm.icu.impl.locale;
8
9import java.util.EnumSet;
10import java.util.HashMap;
11import java.util.HashSet;
12import java.util.Map;
13import java.util.MissingResourceException;
14import java.util.Set;
15import java.util.regex.Pattern;
16
17import com.ibm.icu.impl.ICUResourceBundle;
18import com.ibm.icu.util.Output;
19import com.ibm.icu.util.UResourceBundle;
20import com.ibm.icu.util.UResourceBundleIterator;
21
22/**
23 */
24public class KeyTypeData {
25
26    private static abstract class SpecialTypeHandler {
27        abstract boolean isValid(String value);
28        String canonicalize(String value) {
29            return AsciiUtil.toLowerString(value);
30        }
31    }
32
33    private static class CodepointsTypeHandler extends SpecialTypeHandler {
34        private static final Pattern pat = Pattern.compile("[0-9a-fA-F]{4,6}(-[0-9a-fA-F]{4,6})*");
35        boolean isValid(String value) {
36            return pat.matcher(value).matches();
37        }
38    }
39
40    private static class ReorderCodeTypeHandler extends SpecialTypeHandler {
41        private static final Pattern pat = Pattern.compile("[a-zA-Z]{3,8}(-[a-zA-Z]{3,8})*");
42        boolean isValid(String value) {
43            return pat.matcher(value).matches();
44        }
45    }
46
47    private enum SpecialType {
48        CODEPOINTS(new CodepointsTypeHandler()),
49        REORDER_CODE(new ReorderCodeTypeHandler());
50
51        SpecialTypeHandler handler;
52        SpecialType(SpecialTypeHandler handler) {
53            this.handler = handler;
54        }
55    };
56
57    private static class KeyData {
58        String legacyId;
59        String bcpId;
60        Map<String, Type> typeMap;
61        EnumSet<SpecialType> specialTypes;
62
63        KeyData(String legacyId, String bcpId, Map<String, Type> typeMap,
64                EnumSet<SpecialType> specialTypes) {
65            this.legacyId = legacyId;
66            this.bcpId = bcpId;
67            this.typeMap = typeMap;
68            this.specialTypes = specialTypes;
69        }
70    }
71
72    private static class Type {
73        String legacyId;
74        String bcpId;
75
76        Type(String legacyId, String bcpId) {
77            this.legacyId = legacyId;
78            this.bcpId = bcpId;
79        }
80    }
81
82    public static String toBcpKey(String key) {
83        key = AsciiUtil.toLowerString(key);
84        KeyData keyData = KEYMAP.get(key);
85        if (keyData != null) {
86            return keyData.bcpId;
87        }
88        return null;
89    }
90
91    public static String toLegacyKey(String key) {
92        key = AsciiUtil.toLowerString(key);
93        KeyData keyData = KEYMAP.get(key);
94        if (keyData != null) {
95            return keyData.legacyId;
96        }
97        return null;
98    }
99
100    public static String toBcpType(String key, String type,
101            Output<Boolean> isKnownKey, Output<Boolean> isSpecialType) {
102
103        if (isKnownKey != null) {
104            isKnownKey.value = false;
105        }
106        if (isSpecialType != null) {
107            isSpecialType.value = false;
108        }
109
110        key = AsciiUtil.toLowerString(key);
111        type = AsciiUtil.toLowerString(type);
112
113        KeyData keyData = KEYMAP.get(key);
114        if (keyData != null) {
115            if (isKnownKey != null) {
116                isKnownKey.value = Boolean.TRUE;
117            }
118            Type t = keyData.typeMap.get(type);
119            if (t != null) {
120                return t.bcpId;
121            }
122            if (keyData.specialTypes != null) {
123                for (SpecialType st : keyData.specialTypes) {
124                    if (st.handler.isValid(type)) {
125                        if (isSpecialType != null) {
126                            isSpecialType.value = true;
127                        }
128                        return st.handler.canonicalize(type);
129                    }
130                }
131            }
132        }
133        return null;
134    }
135
136
137    public static String toLegacyType(String key, String type,
138            Output<Boolean> isKnownKey, Output<Boolean> isSpecialType) {
139
140        if (isKnownKey != null) {
141            isKnownKey.value = false;
142        }
143        if (isSpecialType != null) {
144            isSpecialType.value = false;
145        }
146
147        key = AsciiUtil.toLowerString(key);
148        type = AsciiUtil.toLowerString(type);
149
150        KeyData keyData = KEYMAP.get(key);
151        if (keyData != null) {
152            if (isKnownKey != null) {
153                isKnownKey.value = Boolean.TRUE;
154            }
155            Type t = keyData.typeMap.get(type);
156            if (t != null) {
157                return t.legacyId;
158            }
159            if (keyData.specialTypes != null) {
160                for (SpecialType st : keyData.specialTypes) {
161                    if (st.handler.isValid(type)) {
162                        if (isSpecialType != null) {
163                            isSpecialType.value = true;
164                        }
165                        return st.handler.canonicalize(type);
166                    }
167                }
168            }
169        }
170        return null;
171    }
172
173
174    private static void initFromResourceBundle() {
175        UResourceBundle keyTypeDataRes = UResourceBundle.getBundleInstance(
176                ICUResourceBundle.ICU_BASE_NAME,
177                "keyTypeData",
178                ICUResourceBundle.ICU_DATA_CLASS_LOADER);
179        UResourceBundle keyMapRes = keyTypeDataRes.get("keyMap");
180        UResourceBundle typeMapRes = keyTypeDataRes.get("typeMap");
181
182        // alias data is optional
183        UResourceBundle typeAliasRes = null;
184        UResourceBundle bcpTypeAliasRes = null;
185
186        try {
187            typeAliasRes = keyTypeDataRes.get("typeAlias");
188        } catch (MissingResourceException e) {
189            // fall through
190        }
191
192        try {
193            bcpTypeAliasRes = keyTypeDataRes.get("bcpTypeAlias");
194        } catch (MissingResourceException e) {
195            // fall through
196        }
197
198        // iterate through keyMap resource
199        UResourceBundleIterator keyMapItr = keyMapRes.getIterator();
200        while (keyMapItr.hasNext()) {
201            UResourceBundle keyMapEntry = keyMapItr.next();
202            String legacyKeyId = keyMapEntry.getKey();
203            String bcpKeyId = keyMapEntry.getString();
204
205            boolean hasSameKey = false;
206            if (bcpKeyId.length() == 0) {
207                // Empty value indicates that BCP key is same with the legacy key.
208                bcpKeyId = legacyKeyId;
209                hasSameKey = true;
210            }
211
212            boolean isTZ = legacyKeyId.equals("timezone");
213
214            // reverse type alias map
215            Map<String, Set<String>> typeAliasMap = null;
216            if (typeAliasRes != null) {
217                UResourceBundle typeAliasResByKey = null;
218                try {
219                    typeAliasResByKey = typeAliasRes.get(legacyKeyId);
220                } catch (MissingResourceException e) {
221                    // fall through
222                }
223                if (typeAliasResByKey != null) {
224                    typeAliasMap = new HashMap<String, Set<String>>();
225                    UResourceBundleIterator typeAliasResItr = typeAliasResByKey.getIterator();
226                    while (typeAliasResItr.hasNext()) {
227                        UResourceBundle typeAliasDataEntry = typeAliasResItr.next();
228                        String from = typeAliasDataEntry.getKey();
229                        String to = typeAliasDataEntry.getString();
230                        if (isTZ) {
231                            from = from.replace(':', '/');
232                        }
233                        Set<String> aliasSet = typeAliasMap.get(to);
234                        if (aliasSet == null) {
235                            aliasSet = new HashSet<String>();
236                            typeAliasMap.put(to, aliasSet);
237                        }
238                        aliasSet.add(from);
239                    }
240                }
241            }
242
243            // reverse bcp type alias map
244            Map<String, Set<String>> bcpTypeAliasMap = null;
245            if (bcpTypeAliasRes != null) {
246                UResourceBundle bcpTypeAliasResByKey = null;
247                try {
248                    bcpTypeAliasResByKey = bcpTypeAliasRes.get(bcpKeyId);
249                } catch (MissingResourceException e) {
250                    // fall through
251                }
252                if (bcpTypeAliasResByKey != null) {
253                    bcpTypeAliasMap = new HashMap<String, Set<String>>();
254                    UResourceBundleIterator bcpTypeAliasResItr = bcpTypeAliasResByKey.getIterator();
255                    while (bcpTypeAliasResItr.hasNext()) {
256                        UResourceBundle bcpTypeAliasDataEntry = bcpTypeAliasResItr.next();
257                        String from = bcpTypeAliasDataEntry.getKey();
258                        String to = bcpTypeAliasDataEntry.getString();
259                        Set<String> aliasSet = bcpTypeAliasMap.get(to);
260                        if (aliasSet == null) {
261                            aliasSet = new HashSet<String>();
262                            bcpTypeAliasMap.put(to, aliasSet);
263                        }
264                        aliasSet.add(from);
265                    }
266                }
267            }
268
269            Map<String, Type> typeDataMap = new HashMap<String, Type>();
270            Set<SpecialType> specialTypeSet = null;
271
272            // look up type map for the key, and walk through the mapping data
273            UResourceBundle typeMapResByKey = null;
274            try {
275                typeMapResByKey = typeMapRes.get(legacyKeyId);
276            } catch (MissingResourceException e) {
277                // type map for each key must exist
278                assert false;
279            }
280            if (typeMapResByKey != null) {
281                UResourceBundleIterator typeMapResByKeyItr = typeMapResByKey.getIterator();
282                while (typeMapResByKeyItr.hasNext()) {
283                    UResourceBundle typeMapEntry = typeMapResByKeyItr.next();
284                    String legacyTypeId = typeMapEntry.getKey();
285
286                    // special types
287                    boolean isSpecialType = false;
288                    for (SpecialType st : SpecialType.values()) {
289                        if (legacyTypeId.equals(st.toString())) {
290                            isSpecialType = true;
291                            if (specialTypeSet == null) {
292                                specialTypeSet = new HashSet<SpecialType>();
293                            }
294                            specialTypeSet.add(st);
295                            break;
296                        }
297                    }
298                    if (isSpecialType) {
299                        continue;
300                    }
301
302                    if (isTZ) {
303                        // a timezone key uses a colon instead of a slash in the resource.
304                        // e.g. America:Los_Angeles
305                        legacyTypeId = legacyTypeId.replace(':', '/');
306                    }
307
308                    String bcpTypeId = typeMapEntry.getString();
309
310                    boolean hasSameType = false;
311                    if (bcpTypeId.length() == 0) {
312                        // Empty value indicates that BCP type is same with the legacy type.
313                        bcpTypeId = legacyTypeId;
314                        hasSameType = true;
315                    }
316
317                    // Note: legacy type value should never be
318                    // equivalent to bcp type value of a different
319                    // type under the same key. So we use a single
320                    // map for lookup.
321                    Type t = new Type(legacyTypeId, bcpTypeId);
322                    typeDataMap.put(AsciiUtil.toLowerString(legacyTypeId), t);
323                    if (!hasSameType) {
324                        typeDataMap.put(AsciiUtil.toLowerString(bcpTypeId), t);
325                    }
326
327                    // Also put aliases in the map
328                    if (typeAliasMap != null) {
329                        Set<String> typeAliasSet = typeAliasMap.get(legacyTypeId);
330                        if (typeAliasSet != null) {
331                            for (String alias : typeAliasSet) {
332                                typeDataMap.put(AsciiUtil.toLowerString(alias), t);
333                            }
334                        }
335                    }
336                    if (bcpTypeAliasMap != null) {
337                        Set<String> bcpTypeAliasSet = bcpTypeAliasMap.get(bcpTypeId);
338                        if (bcpTypeAliasSet != null) {
339                            for (String alias : bcpTypeAliasSet) {
340                                typeDataMap.put(AsciiUtil.toLowerString(alias), t);
341                            }
342                        }
343                    }
344                }
345            }
346
347            EnumSet<SpecialType> specialTypes = null;
348            if (specialTypeSet != null) {
349                specialTypes = EnumSet.copyOf(specialTypeSet);
350            }
351
352            KeyData keyData = new KeyData(legacyKeyId, bcpKeyId, typeDataMap, specialTypes);
353
354            KEYMAP.put(AsciiUtil.toLowerString(legacyKeyId), keyData);
355            if (!hasSameKey) {
356                KEYMAP.put(AsciiUtil.toLowerString(bcpKeyId), keyData);
357            }
358        }
359    }
360
361    //
362    // Note:    The key-type data is currently read from ICU resource bundle keyTypeData.res.
363    //          In future, we may import the data into code like below directly from CLDR to
364    //          avoid cyclic dependency between ULocale and UResourceBundle. For now, the code
365    //          below is just for proof of concept, and commented out.
366    //
367
368//    private static final String[][] TYPE_DATA_CA = {
369//     // {<legacy type>, <bcp type - if different>},
370//        {"buddhist", null},
371//        {"chinese", null},
372//        {"coptic", null},
373//        {"dangi", null},
374//        {"ethiopic", null},
375//        {"ethiopic-amete-alem", "ethioaa"},
376//        {"gregorian", "gregory"},
377//        {"hebrew", null},
378//        {"indian", null},
379//        {"islamic", null},
380//        {"islamic-civil", null},
381//        {"islamic-rgsa", null},
382//        {"islamic-tbla", null},
383//        {"islamic-umalqura", null},
384//        {"iso8601", null},
385//        {"japanese", null},
386//        {"persian", null},
387//        {"roc", null},
388//    };
389//
390//    private static final String[][] TYPE_DATA_KS = {
391//     // {<legacy type>, <bcp type - if different>},
392//        {"identical", "identic"},
393//        {"primary", "level1"},
394//        {"quaternary", "level4"},
395//        {"secondary", "level2"},
396//        {"tertiary", "level3"},
397//    };
398//
399//    private static final String[][] TYPE_ALIAS_KS = {
400//     // {<legacy alias>, <legacy canonical>},
401//        {"quarternary", "quaternary"},
402//    };
403//
404//    private static final String[][] BCP_TYPE_ALIAS_CA = {
405//     // {<bcp deprecated>, <bcp preferred>
406//        {"islamicc", "islamic-civil"},
407//    };
408//
409//    private static final Object[][] KEY_DATA = {
410//     // {<legacy key>, <bcp key - if different>, <type map>, <type alias>, <bcp type alias>},
411//        {"calendar", "ca", TYPE_DATA_CA, null, BCP_TYPE_ALIAS_CA},
412//        {"colstrength", "ks", TYPE_DATA_KS, TYPE_ALIAS_KS, null},
413//    };
414
415    private static final Object[][] KEY_DATA = {};
416
417    @SuppressWarnings("unused")
418    private static void initFromTables() {
419        for (Object[] keyDataEntry : KEY_DATA) {
420            String legacyKeyId = (String)keyDataEntry[0];
421            String bcpKeyId = (String)keyDataEntry[1];
422            String[][] typeData = (String[][])keyDataEntry[2];
423            String[][] typeAliasData = (String[][])keyDataEntry[3];
424            String[][] bcpTypeAliasData = (String[][])keyDataEntry[4];
425
426            boolean hasSameKey = false;
427            if (bcpKeyId == null) {
428                bcpKeyId = legacyKeyId;
429                hasSameKey = true;
430            }
431
432            // reverse type alias map
433            Map<String, Set<String>> typeAliasMap = null;
434            if (typeAliasData != null) {
435                typeAliasMap = new HashMap<String, Set<String>>();
436                for (String[] typeAliasDataEntry : typeAliasData) {
437                    String from = typeAliasDataEntry[0];
438                    String to = typeAliasDataEntry[1];
439                    Set<String> aliasSet = typeAliasMap.get(to);
440                    if (aliasSet == null) {
441                        aliasSet = new HashSet<String>();
442                        typeAliasMap.put(to, aliasSet);
443                    }
444                    aliasSet.add(from);
445                }
446            }
447
448            // BCP type alias map data
449            Map<String, Set<String>> bcpTypeAliasMap = null;
450            if (bcpTypeAliasData != null) {
451                bcpTypeAliasMap = new HashMap<String, Set<String>>();
452                for (String[] bcpTypeAliasDataEntry : bcpTypeAliasData) {
453                    String from = bcpTypeAliasDataEntry[0];
454                    String to = bcpTypeAliasDataEntry[1];
455                    Set<String> aliasSet = bcpTypeAliasMap.get(to);
456                    if (aliasSet == null) {
457                        aliasSet = new HashSet<String>();
458                        bcpTypeAliasMap.put(to, aliasSet);
459                    }
460                    aliasSet.add(from);
461                }
462            }
463
464            // Type map data
465            assert typeData != null;
466            Map<String, Type> typeDataMap = new HashMap<String, Type>();
467            Set<SpecialType> specialTypeSet = null;
468
469            for (String[] typeDataEntry : typeData) {
470                String legacyTypeId = typeDataEntry[0];
471                String bcpTypeId = typeDataEntry[1];
472
473                // special types
474                boolean isSpecialType = false;
475                for (SpecialType st : SpecialType.values()) {
476                    if (legacyTypeId.equals(st.toString())) {
477                        isSpecialType = true;
478                        if (specialTypeSet == null) {
479                            specialTypeSet = new HashSet<SpecialType>();
480                        }
481                        specialTypeSet.add(st);
482                        break;
483                    }
484                }
485                if (isSpecialType) {
486                    continue;
487                }
488
489                boolean hasSameType = false;
490                if (bcpTypeId == null) {
491                    bcpTypeId = legacyTypeId;
492                    hasSameType = true;
493                }
494
495                // Note: legacy type value should never be
496                // equivalent to bcp type value of a different
497                // type under the same key. So we use a single
498                // map for lookup.
499                Type t = new Type(legacyTypeId, bcpTypeId);
500                typeDataMap.put(AsciiUtil.toLowerString(legacyTypeId), t);
501                if (!hasSameType) {
502                    typeDataMap.put(AsciiUtil.toLowerString(bcpTypeId), t);
503                }
504
505                // Also put aliases in the index
506                Set<String> typeAliasSet = typeAliasMap.get(legacyTypeId);
507                if (typeAliasSet != null) {
508                    for (String alias : typeAliasSet) {
509                        typeDataMap.put(AsciiUtil.toLowerString(alias), t);
510                    }
511                }
512                Set<String> bcpTypeAliasSet = bcpTypeAliasMap.get(bcpTypeId);
513                if (bcpTypeAliasSet != null) {
514                    for (String alias : bcpTypeAliasSet) {
515                        typeDataMap.put(AsciiUtil.toLowerString(alias), t);
516                    }
517                }
518            }
519
520            EnumSet<SpecialType> specialTypes = null;
521            if (specialTypeSet != null) {
522                specialTypes = EnumSet.copyOf(specialTypeSet);
523            }
524
525            KeyData keyData = new KeyData(legacyKeyId, bcpKeyId, typeDataMap, specialTypes);
526
527            KEYMAP.put(AsciiUtil.toLowerString(legacyKeyId), keyData);
528            if (!hasSameKey) {
529                KEYMAP.put(AsciiUtil.toLowerString(bcpKeyId), keyData);
530            }
531        }
532    }
533
534    private static final Map<String, KeyData> KEYMAP;
535
536    static {
537        KEYMAP = new HashMap<String, KeyData>();
538//        initFromTables();
539        initFromResourceBundle();
540    }
541
542}
543