1 /* 2 * Copyright (c) 2012, 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.cldrconverter; 27 28 import java.util.ArrayList; 29 import java.util.Arrays; 30 import java.util.EnumSet; 31 import java.util.HashMap; 32 import java.util.List; 33 import java.util.Map; 34 35 class Bundle { 36 static enum Type { 37 LOCALENAMES, CURRENCYNAMES, TIMEZONENAMES, CALENDARDATA, FORMATDATA; 38 39 static EnumSet<Type> ALL_TYPES = EnumSet.of(LOCALENAMES, 40 CURRENCYNAMES, 41 TIMEZONENAMES, 42 CALENDARDATA, 43 FORMATDATA); 44 } 45 46 private final static Map<String, Bundle> bundles = new HashMap<>(); 47 48 private final static String[] NUMBER_PATTERN_KEYS = { 49 "NumberPatterns/decimal", 50 "NumberPatterns/currency", 51 "NumberPatterns/percent" 52 }; 53 54 private final static String[] NUMBER_ELEMENT_KEYS = { 55 "NumberElements/decimal", 56 "NumberElements/group", 57 "NumberElements/list", 58 "NumberElements/percent", 59 "NumberElements/zero", 60 "NumberElements/pattern", 61 "NumberElements/minus", 62 "NumberElements/exponential", 63 "NumberElements/permille", 64 "NumberElements/infinity", 65 "NumberElements/nan" 66 }; 67 68 private final static String[] TIME_PATTERN_KEYS = { 69 "DateTimePatterns/full-time", 70 "DateTimePatterns/long-time", 71 "DateTimePatterns/medium-time", 72 "DateTimePatterns/short-time", 73 }; 74 75 private final static String[] DATE_PATTERN_KEYS = { 76 "DateTimePatterns/full-date", 77 "DateTimePatterns/long-date", 78 "DateTimePatterns/medium-date", 79 "DateTimePatterns/short-date", 80 }; 81 82 private final static String[] DATETIME_PATTERN_KEYS = { 83 "DateTimePatterns/date-time" 84 }; 85 86 private final static String[] ERA_KEYS = { 87 "long.Eras", 88 "Eras", 89 "short.Eras" 90 }; 91 92 private final String id; 93 private final String cldrPath; 94 private final EnumSet<Type> bundleTypes; 95 private final String currencies; 96 97 static Bundle getBundle(String id) { 98 return bundles.get(id); 99 } 100 101 Bundle(String id, String cldrPath, String bundles, String currencies) { 102 this.id = id; 103 this.cldrPath = cldrPath; 104 if ("localenames".equals(bundles)) { 105 bundleTypes = EnumSet.of(Type.LOCALENAMES); 106 } else if ("currencynames".equals(bundles)) { 107 bundleTypes = EnumSet.of(Type.CURRENCYNAMES); 108 } else { 109 bundleTypes = Type.ALL_TYPES; 110 } 111 if (currencies == null) { 112 currencies = "local"; 113 } 114 this.currencies = currencies; 115 addBundle(); 116 } 117 118 private void addBundle() { 119 Bundle.bundles.put(id, this); 120 } 121 122 String getID() { 123 return id; 124 } 125 126 boolean isRoot() { 127 return "root".equals(id); 128 } 129 130 String getCLDRPath() { 131 return cldrPath; 132 } 133 134 EnumSet<Type> getBundleTypes() { 135 return bundleTypes; 136 } 137 138 String getCurrencies() { 139 return currencies; 140 } 141 142 /** 143 * Generate a map that contains all the data that should be 144 * visible for the bundle's locale 145 */ 146 Map<String, Object> getTargetMap() throws Exception { 147 String[] cldrBundles = getCLDRPath().split(","); 148 149 // myMap contains resources for id. 150 Map<String, Object> myMap = new HashMap<>(); 151 int index; 152 for (index = 0; index < cldrBundles.length; index++) { 153 if (cldrBundles[index].equals(id)) { 154 myMap.putAll(CLDRConverter.getCLDRBundle(cldrBundles[index])); 155 break; 156 } 157 } 158 159 // parentsMap contains resources from id's parents. 160 Map<String, Object> parentsMap = new HashMap<>(); 161 for (int i = cldrBundles.length - 1; i > index; i--) { 162 if (!("no".equals(cldrBundles[i]) || cldrBundles[i].startsWith("no_"))) { 163 parentsMap.putAll(CLDRConverter.getCLDRBundle(cldrBundles[i])); 164 } 165 } 166 // Duplicate myMap as parentsMap for "root" so that the 167 // fallback works. This is a huck, though. 168 if ("root".equals(cldrBundles[0])) { 169 assert parentsMap.isEmpty(); 170 parentsMap.putAll(myMap); 171 } 172 173 // merge individual strings into arrays 174 175 // if myMap has any of the NumberPatterns members 176 for (String k : NUMBER_PATTERN_KEYS) { 177 if (myMap.containsKey(k)) { 178 String[] numberPatterns = new String[NUMBER_PATTERN_KEYS.length]; 179 for (int i = 0; i < NUMBER_PATTERN_KEYS.length; i++) { 180 String key = NUMBER_PATTERN_KEYS[i]; 181 String value = (String) myMap.remove(key); 182 if (value == null) { 183 value = (String) parentsMap.remove(key); 184 } 185 if (value.length() == 0) { 186 CLDRConverter.warning("empty pattern for " + key); 187 } 188 numberPatterns[i] = value; 189 } 190 myMap.put("NumberPatterns", numberPatterns); 191 break; 192 } 193 } 194 195 // if myMap has any of NUMBER_ELEMENT_KEYS, create a complete NumberElements. 196 String defaultScript = (String) myMap.get("DefaultNumberingSystem"); 197 @SuppressWarnings("unchecked") 198 List<String> scripts = (List<String>) myMap.get("numberingScripts"); 199 if (defaultScript == null && scripts != null) { 200 // Some locale data has no default script for numbering even with mutiple scripts. 201 // Take the first one as default in that case. 202 defaultScript = scripts.get(0); 203 myMap.put("DefaultNumberingSystem", defaultScript); 204 } 205 if (scripts != null) { 206 for (String script : scripts) { 207 for (String k : NUMBER_ELEMENT_KEYS) { 208 String[] numberElements = new String[NUMBER_ELEMENT_KEYS.length]; 209 for (int i = 0; i < NUMBER_ELEMENT_KEYS.length; i++) { 210 String key = script + "." + NUMBER_ELEMENT_KEYS[i]; 211 String value = (String) myMap.remove(key); 212 if (value == null) { 213 if (key.endsWith("/pattern")) { 214 value = "#"; 215 } else { 216 value = (String) parentsMap.get(key); 217 if (value == null) { 218 // the last resort is "latn" 219 key = "latn." + NUMBER_ELEMENT_KEYS[i]; 220 value = (String) parentsMap.get(key); 221 if (value == null) { 222 throw new InternalError("NumberElements: null for " + key); 223 } 224 } 225 } 226 } 227 numberElements[i] = value; 228 } 229 myMap.put(script + "." + "NumberElements", numberElements); 230 break; 231 } 232 } 233 } 234 235 // another hack: parentsMap is not used for date-time resources. 236 if ("root".equals(id)) { 237 parentsMap = null; 238 } 239 240 for (CalendarType calendarType : CalendarType.values()) { 241 String calendarPrefix = calendarType.keyElementName(); 242 // handle multiple inheritance for month and day names 243 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "MonthNames"); 244 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "MonthAbbreviations"); 245 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "DayNames"); 246 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "DayAbbreviations"); 247 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "AmPmMarkers"); 248 249 adjustEraNames(myMap, calendarType); 250 251 handleDateTimeFormatPatterns(TIME_PATTERN_KEYS, myMap, parentsMap, calendarType, "TimePatterns"); 252 handleDateTimeFormatPatterns(DATE_PATTERN_KEYS, myMap, parentsMap, calendarType, "DatePatterns"); 253 handleDateTimeFormatPatterns(DATETIME_PATTERN_KEYS, myMap, parentsMap, calendarType, "DateTimePatterns"); 254 } 255 256 return myMap; 257 } 258 259 private void handleMultipleInheritance(Map<String, Object> map, Map<String, Object> parents, String key) { 260 String formatKey = key + "/format"; 261 Object format = map.get(formatKey); 262 if (format != null) { 263 map.remove(formatKey); 264 map.put(key, format); 265 if (fillInElements(parents, formatKey, format)) { 266 map.remove(key); 267 } 268 } 269 String standaloneKey = key + "/stand-alone"; 270 Object standalone = map.get(standaloneKey); 271 if (standalone != null) { 272 map.remove(standaloneKey); 273 String realKey = key; 274 if (format != null) { 275 realKey = "standalone." + key; 276 } 277 map.put(realKey, standalone); 278 if (fillInElements(parents, standaloneKey, standalone)) { 279 map.remove(realKey); 280 } 281 } 282 } 283 284 /** 285 * Fills in any empty elements with its parent element. Returns true if the resulting array is 286 * identical to its parent array. 287 * 288 * @param parents 289 * @param key 290 * @param value 291 * @return true if the resulting array is identical to its parent array. 292 */ 293 private boolean fillInElements(Map<String, Object> parents, String key, Object value) { 294 if (parents == null) { 295 return false; 296 } 297 if (value instanceof String[]) { 298 Object pvalue = parents.get(key); 299 if (pvalue != null && pvalue instanceof String[]) { 300 String[] strings = (String[]) value; 301 String[] pstrings = (String[]) pvalue; 302 for (int i = 0; i < strings.length; i++) { 303 if (strings[i] == null || strings[i].length() == 0) { 304 strings[i] = pstrings[i]; 305 } 306 } 307 return Arrays.equals(strings, pstrings); 308 } 309 } 310 return false; 311 } 312 313 /* 314 * Adjusts String[] for era names because JRE's Calendars use different 315 * ERA value indexes in the Buddhist and Japanese Imperial calendars. 316 */ 317 private void adjustEraNames(Map<String, Object> map, CalendarType type) { 318 String[][] eraNames = new String[ERA_KEYS.length][]; 319 String[] realKeys = new String[ERA_KEYS.length]; 320 int index = 0; 321 for (String key : ERA_KEYS) { 322 String realKey = type.keyElementName() + key; 323 String[] value = (String[]) map.get(realKey); 324 if (value != null) { 325 switch (type) { 326 case GREGORIAN: 327 break; 328 329 case JAPANESE: 330 { 331 String[] newValue = new String[value.length + 1]; 332 String[] julianEras = (String[]) map.get(key); 333 if (julianEras != null && julianEras.length >= 2) { 334 newValue[0] = julianEras[1]; 335 } else { 336 newValue[0] = ""; 337 } 338 System.arraycopy(value, 0, newValue, 1, value.length); 339 value = newValue; 340 } 341 break; 342 343 case BUDDHIST: 344 // Replace the value 345 value = new String[] {"BC", value[0]}; 346 break; 347 } 348 if (!key.equals(realKey)) { 349 map.put(realKey, value); 350 } 351 } 352 realKeys[index] = realKey; 353 eraNames[index++] = value; 354 } 355 if (eraNames[0] != null) { 356 if (eraNames[1] != null) { 357 if (eraNames[2] == null) { 358 // Eras -> short.Eras 359 // long.Eras -> Eras 360 map.put(realKeys[2], map.get(realKeys[1])); 361 map.put(realKeys[1], map.get(realKeys[0])); 362 } 363 } else { 364 // long.Eras -> Eras 365 map.put(realKeys[1], map.get(realKeys[0])); 366 } 367 // remove long.Eras 368 map.remove(realKeys[0]); 369 } 370 } 371 372 private void handleDateTimeFormatPatterns(String[] patternKeys, Map<String, Object> myMap, Map<String, Object> parentsMap, 373 CalendarType calendarType, String name) { 374 String calendarPrefix = calendarType.keyElementName(); 375 for (String k : patternKeys) { 376 if (myMap.containsKey(calendarPrefix + k)) { 377 int len = patternKeys.length; 378 List<String> patterns = new ArrayList<>(); 379 for (int i = 0; i < len; i++) { 380 String key = calendarPrefix + patternKeys[i]; 381 String pattern = (String) myMap.remove(key); 382 if (pattern == null) { 383 pattern = (String) parentsMap.remove(key); 384 } 385 if (pattern != null) { 386 patterns.add(i, translateDateFormatLetters(calendarType, pattern)); 387 } 388 } 389 if (patterns.isEmpty()) { 390 return; 391 } 392 String key = calendarPrefix + name; 393 myMap.put(key, patterns.toArray(new String[len])); 394 break; 395 } 396 } 397 } 398 399 private String translateDateFormatLetters(CalendarType calendarType, String cldrFormat) { 400 String pattern = cldrFormat; 401 int length = pattern.length(); 402 boolean inQuote = false; 403 StringBuilder jrePattern = new StringBuilder(length); 404 int count = 0; 405 char lastLetter = 0; 406 407 for (int i = 0; i < length; i++) { 408 char c = pattern.charAt(i); 409 410 if (c == '\'') { 411 // '' is treated as a single quote regardless of being 412 // in a quoted section. 413 if ((i + 1) < length) { 414 char nextc = pattern.charAt(i + 1); 415 if (nextc == '\'') { 416 i++; 417 if (count != 0) { 418 convert(calendarType, lastLetter, count, jrePattern); 419 lastLetter = 0; 420 count = 0; 421 } 422 jrePattern.append("''"); 423 continue; 424 } 425 } 426 if (!inQuote) { 427 if (count != 0) { 428 convert(calendarType, lastLetter, count, jrePattern); 429 lastLetter = 0; 430 count = 0; 431 } 432 inQuote = true; 433 } else { 434 inQuote = false; 435 } 436 jrePattern.append(c); 437 continue; 438 } 439 if (inQuote) { 440 jrePattern.append(c); 441 continue; 442 } 443 if (!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')) { 444 if (count != 0) { 445 convert(calendarType, lastLetter, count, jrePattern); 446 lastLetter = 0; 447 count = 0; 448 } 449 jrePattern.append(c); 450 continue; 451 } 452 453 if (lastLetter == 0 || lastLetter == c) { 454 lastLetter = c; 455 count++; 456 continue; 457 } 458 convert(calendarType, lastLetter, count, jrePattern); 459 lastLetter = c; 460 count = 1; 461 } 462 463 if (inQuote) { 464 throw new InternalError("Unterminated quote in date-time pattern: " + cldrFormat); 465 } 466 467 if (count != 0) { 468 convert(calendarType, lastLetter, count, jrePattern); 469 } 470 if (cldrFormat.contentEquals(jrePattern)) { 471 return cldrFormat; 472 } 473 return jrePattern.toString(); 474 } 475 476 private void convert(CalendarType calendarType, char cldrLetter, int count, StringBuilder sb) { 477 switch (cldrLetter) { 478 case 'G': 479 if (calendarType != CalendarType.GREGORIAN) { 480 // Adjust the number of 'G's for JRE SimpleDateFormat 481 if (count == 5) { 482 // CLDR narrow -> JRE short 483 count = 1; 484 } else if (count == 1) { 485 // CLDR abbr -> JRE long 486 count = 4; 487 } 488 } 489 appendN(cldrLetter, count, sb); 490 break; 491 492 // TODO: support 'c' and 'e' in JRE SimpleDateFormat 493 // Use 'u' and 'E' for now. 494 case 'c': 495 case 'e': 496 switch (count) { 497 case 1: 498 sb.append('u'); 499 break; 500 case 3: 501 case 4: 502 appendN('E', count, sb); 503 break; 504 case 5: 505 appendN('E', 3, sb); 506 break; 507 } 508 break; 509 510 case 'v': 511 case 'V': 512 appendN('z', count, sb); 513 break; 514 515 case 'Z': 516 if (count == 4 || count == 5) { 517 sb.append("XXX"); 518 } 519 break; 520 521 case 'u': 522 case 'U': 523 case 'q': 524 case 'Q': 525 case 'l': 526 case 'g': 527 case 'j': 528 case 'A': 529 throw new InternalError(String.format("Unsupported letter: '%c', count=%d%n", 530 cldrLetter, count)); 531 default: 532 appendN(cldrLetter, count, sb); 533 break; 534 } 535 } 536 537 private void appendN(char c, int n, StringBuilder sb) { 538 for (int i = 0; i < n; i++) { 539 sb.append(c); 540 } 541 } 542 }