1 /*
2  *******************************************************************************
3  * Copyright (C) 2014, International Business Machines Corporation and
4  * others. All Rights Reserved.
5  *******************************************************************************
6  */
7 package com.ibm.icu.impl.locale;
8 
9 import java.util.EnumSet;
10 import java.util.HashMap;
11 import java.util.HashSet;
12 import java.util.Map;
13 import java.util.MissingResourceException;
14 import java.util.Set;
15 import java.util.regex.Pattern;
16 
17 import com.ibm.icu.impl.ICUResourceBundle;
18 import com.ibm.icu.util.Output;
19 import com.ibm.icu.util.UResourceBundle;
20 import com.ibm.icu.util.UResourceBundleIterator;
21 
22 /**
23  */
24 public class KeyTypeData {
25 
26     private static abstract class SpecialTypeHandler {
isValid(String value)27         abstract boolean isValid(String value);
canonicalize(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})*");
isValid(String value)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})*");
isValid(String value)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;
SpecialType(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 
KeyData(String legacyId, String bcpId, Map<String, Type> typeMap, EnumSet<SpecialType> specialTypes)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 
Type(String legacyId, String bcpId)76         Type(String legacyId, String bcpId) {
77             this.legacyId = legacyId;
78             this.bcpId = bcpId;
79         }
80     }
81 
toBcpKey(String key)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 
toLegacyKey(String key)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 
toBcpType(String key, String type, Output<Boolean> isKnownKey, Output<Boolean> isSpecialType)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 
toLegacyType(String key, String type, Output<Boolean> isKnownKey, Output<Boolean> isSpecialType)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 
initFromResourceBundle()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")
initFromTables()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();
initFromResourceBundle()539         initFromResourceBundle();
540     }
541 
542 }
543