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 }