1 package org.unicode.cldr.json; 2 3 import com.google.common.collect.ImmutableSet; 4 import java.util.ArrayList; 5 import java.util.HashSet; 6 import java.util.Iterator; 7 import java.util.List; 8 import java.util.Set; 9 import java.util.concurrent.atomic.AtomicInteger; 10 import java.util.regex.Matcher; 11 import java.util.regex.Pattern; 12 import org.unicode.cldr.util.Builder; 13 import org.unicode.cldr.util.CLDRFile; 14 import org.unicode.cldr.util.FileProcessor; 15 import org.unicode.cldr.util.PatternCache; 16 17 class LdmlConvertRules { 18 19 /** File sets that will not be processed in JSON transformation. */ 20 public static final ImmutableSet<String> IGNORE_FILE_SET = 21 ImmutableSet.of( 22 "attributeValueValidity", "coverageLevels", "postalCodeData", "subdivisions"); 23 24 /** 25 * The attribute list that should become part of the name in form of name-(attribute)-(value). 26 * [parent_element]:[element]:[attribute] 27 */ 28 // common/main 29 static final ImmutableSet<String> NAME_PART_DISTINGUISHING_ATTR_SET = 30 ImmutableSet.of( 31 "monthWidth:month:yeartype", 32 "characters:parseLenients:scope", 33 "dateFormat:pattern:numbers", 34 "characterLabelPatterns:characterLabelPattern:count", // originally under 35 // characterLabels 36 "currencyFormats:unitPattern:count", 37 "currency:displayName:count", 38 "numbers:symbols:numberSystem", 39 "numbers:decimalFormats:numberSystem", 40 "numbers:currencyFormats:numberSystem", 41 "numbers:percentFormats:numberSystem", 42 "numbers:scientificFormats:numberSystem", 43 "numbers:miscPatterns:numberSystem", 44 "minimalPairs:pluralMinimalPairs:count", 45 "territoryContainment:group:status", 46 "decimalFormat:pattern:count", 47 "currencyFormat:pattern:count", 48 "unit:unitPattern:count", 49 // compound units 50 "compoundUnit:compoundUnitPattern1:count", 51 "compoundUnit:compoundUnitPattern1:gender", 52 "compoundUnit:compoundUnitPattern1:case", 53 "field:relative:type", 54 "field:relativeTime:type", 55 "relativeTime:relativeTimePattern:count", 56 "availableFormats:dateFormatItem:count", 57 "listPatterns:listPattern:type", 58 "timeZoneNames:regionFormat:type", 59 "units:durationUnit:type", 60 "weekData:minDays:territories", 61 "weekData:firstDay:territories", 62 "weekData:weekendStart:territories", 63 "weekData:weekendEnd:territories", 64 "supplemental:dayPeriodRuleSet:type", 65 // units 66 "unitPreferenceDataData:unitPreferences:category", 67 // grammatical features 68 // in common/supplemental/grammaticalFeatures.xml 69 "grammaticalData:grammaticalFeatures:targets", 70 "grammaticalGenderData:grammaticalFeatures:targets", 71 "grammaticalFeatures:grammaticalCase:scope", 72 "grammaticalFeatures:grammaticalGender:scope", 73 "grammaticalDerivations:deriveCompound:structure", 74 "grammaticalDerivations:deriveCompound:feature", 75 "grammaticalDerivations:deriveComponent:feature", 76 "grammaticalDerivations:deriveComponent:structure", 77 // measurement 78 "measurementData:measurementSystem:category", 79 "supplemental:plurals:type", 80 "pluralRanges:pluralRange:start", 81 "pluralRanges:pluralRange:end", 82 "pluralRules:pluralRule:count", 83 "languageMatches:languageMatch:desired", 84 "styleNames:styleName:subtype", 85 "styleNames:styleName:alt"); 86 87 /** 88 * The set of attributes that should become part of the name in form of 89 * name-(attribute)-(value). 90 */ 91 92 /** 93 * Following is a list of element:attribute pair. These attributes should be treated as values. 94 * For example, <type type="arab" key="numbers">Arabic-Indic Digits</type> should be really 95 * converted as, "arab": { "_value": "Arabic-Indic Digits", "_key": "numbers" } 96 */ 97 static final ImmutableSet<String> ATTR_AS_VALUE_SET = 98 ImmutableSet.of( 99 100 // in common/supplemental/dayPeriods.xml 101 "dayPeriodRules:dayPeriodRule:from", 102 103 // in common/supplemental/likelySubtags.xml 104 "likelySubtags:likelySubtag:to", 105 106 // in common/supplemental/metaZones.xml 107 "timezone:usesMetazone:mzone", 108 // Only the current usesMetazone will be kept, it is not necessary to keep 109 // "to" and "from" attributes to make key unique. This is needed as their 110 // value is not good if used as key. 111 "timezone:usesMetazone:to", 112 "timezone:usesMetazone:from", 113 "mapTimezones:mapZone:other", 114 "mapTimezones:mapZone:type", 115 "mapTimezones:mapZone:territory", 116 117 // in common/supplemental/numberingSystems.xml 118 "numberingSystems:numberingSystem:type", 119 120 // in common/supplemental/supplementalData.xml 121 "region:currency:from", 122 "region:currency:to", 123 "region:currency:tender", 124 "calendar:calendarSystem:type", 125 "codeMappings:territoryCodes:numeric", 126 "codeMappings:territoryCodes:alpha3", 127 "codeMappings:currencyCodes:numeric", 128 "timeData:hours:allowed", 129 "timeData:hours:preferred", 130 // common/supplemental/supplementalMetaData.xml 131 "validity:variable:type", 132 "deprecated:deprecatedItems:elements", 133 "deprecated:deprecatedItems:attributes", 134 "deprecated:deprecatedItems:type", 135 136 // in common/supplemental/telephoneCodeData.xml 137 "codesByTerritory:telephoneCountryCode:code", 138 139 // in common/supplemental/windowsZones.xml 140 "mapTimezones:mapZone:other", 141 142 // in common/supplemental/units.xml 143 "*:unitPreference:geq", 144 "*:unitPreference:skeleton", 145 146 // in common/supplemental/grammaticalFeatures.xml 147 "grammaticalDerivations:deriveComponent:value0", 148 "grammaticalDerivations:deriveComponent:value1", 149 150 // identity elements 151 "identity:language:type", 152 "identity:script:type", 153 "identity:territory:type", 154 "identity:variant:type", 155 156 // in common/bcp47/*.xml 157 "keyword:key:name"); 158 159 /** 160 * The set of element:attribute pair in which the attribute should be treated as value. All the 161 * attribute here are non-distinguishing attributes. 162 */ 163 164 /** 165 * For those attributes that are treated as values, they taken the form of element_name: { ..., 166 * attribute: value, ...} This is desirable as an element may have several attributes that are 167 * treated as values. But in some cases, there is one such attribute only, and it is more 168 * desirable to convert element_name: { attribute: value} to element_name: value With a solid 169 * example, (likelySubtags:likelySubtag:to) <likelySubtag from="zh" to="zh_Hans_CN" /> 170 * distinguishing attr "from" will become the key, its better to omit "to" and have this simple 171 * mapping: "zh" : "zh_Hans_CN", 172 */ 173 static final ImmutableSet<String> COMPACTABLE_ATTR_AS_VALUE_SET = 174 ImmutableSet.of( 175 // parent:element:attribute 176 // common/main 177 "calendars:default:choice", 178 "dateFormats:default:choice", 179 "months:default:choice", 180 "monthContext:default:choice", 181 "days:default:choice", 182 "dayContext:default:choice", 183 "timeFormats:default:choice", 184 "dateTimeFormats:default:choice", 185 "timeZoneNames:singleCountries:list", 186 187 // rbnf 188 "ruleset:rbnfrule:value", 189 // common/supplemental 190 "likelySubtags:likelySubtag:to", 191 // "territoryContainment:group:type", 192 "calendar:calendarSystem:type", 193 "calendarPreferenceData:calendarPreference:ordering", 194 "codesByTerritory:telephoneCountryCode:code", 195 196 // common/collation 197 "collations:default:choice", 198 199 // common/supplemental/pluralRanges.xml 200 "pluralRanges:pluralRange:result", 201 202 // identity elements 203 "identity:language:type", 204 "identity:script:type", 205 "identity:territory:type", 206 "identity:variant:type", 207 "grammaticalFeatures:grammaticalGender:values", 208 "grammaticalFeatures:grammaticalDefiniteness:values", 209 "grammaticalFeatures:grammaticalCase:values", 210 "grammaticalDerivations:deriveCompound:value"); 211 212 /** 213 * The set of attributes that should be treated as value, and reduce to simple value only form. 214 */ 215 216 /** Anonymous key name. */ 217 public static final String ANONYMOUS_KEY = "_"; 218 219 /** 220 * Check if the attribute should be suppressed. 221 * 222 * <p>Right now only "_q" is suppressed. In most cases array is used and there is no need for 223 * this information. In other cases, order is irrelevant. 224 * 225 * @return True if the attribute should be suppressed. 226 */ IsSuppresedAttr(String attr)227 public static boolean IsSuppresedAttr(String attr) { 228 return attr.endsWith("_q") || attr.endsWith("-q"); 229 } 230 231 /** The set of attributes that should be ignored in the conversion process. */ 232 public static final ImmutableSet<String> IGNORABLE_NONDISTINGUISHING_ATTR_SET = 233 ImmutableSet.of("draft", "references", "origin"); 234 235 /** 236 * List of attributes that should be suppressed. This list comes from 237 * cldr/common/supplemental/supplementalMetadata. Each three of them is a group, they are for 238 * element, value and attribute. If the specified attribute appears in specified element with 239 * specified = value, it should be suppressed. 240 */ 241 public static final String[] ATTR_SUPPRESS_LIST = { 242 // common/main 243 "dateFormat", "standard", "type", 244 "dateTimeFormat", "standard", "type", 245 "timeFormat", "standard", "type", 246 "decimalFormat", "standard", "type", 247 "percentFormat", "standard", "type", 248 "scientificFormat", "standard", "type", 249 "pattern", "standard", "type" 250 }; 251 252 /** This is a simple class to hold the splittable attribute specification. */ 253 public static class SplittableAttributeSpec { 254 public String element; 255 public String attribute; 256 public String attrAsValueAfterSplit; 257 SplittableAttributeSpec(String el, String attr, String av)258 SplittableAttributeSpec(String el, String attr, String av) { 259 element = el; 260 attribute = attr; 261 attrAsValueAfterSplit = av; 262 } 263 } 264 265 /** 266 * List of attributes that has value that can be split. Each two of them is a group, and 267 * represent element and value. Occurrences of such match should lead to creation of multiple 268 * node. Example: <weekendStart day="thu" territories="DZ KW OM SA SD YE AF IR"/> should be 269 * treated as if following node is encountered. <weekendStart day="thu" territories="DZ"/> 270 * <weekendStart day="thu" territories="KW"/> <weekendStart day="thu" territories="OM"/> 271 * <weekendStart day="thu" territories="SA"/> <weekendStart day="thu" territories="SD"/> 272 * <weekendStart day="thu" territories="YE"/> <weekendStart day="thu" territories="AF"/> 273 * <weekendStart day="thu" territories="IR"/> 274 */ 275 private static final SplittableAttributeSpec[] SPLITTABLE_ATTRS = { 276 new SplittableAttributeSpec("calendarPreference", "territories", null), 277 new SplittableAttributeSpec("pluralRanges", "locales", null), 278 new SplittableAttributeSpec("pluralRules", "locales", null), 279 new SplittableAttributeSpec("minDays", "territories", "count"), 280 new SplittableAttributeSpec("firstDay", "territories", "day"), 281 new SplittableAttributeSpec("weekendStart", "territories", "day"), 282 new SplittableAttributeSpec("weekendEnd", "territories", "day"), 283 new SplittableAttributeSpec("weekOfPreference", "locales", "ordering"), 284 new SplittableAttributeSpec("measurementSystem", "territories", "type"), 285 // this is deprecated, so no need to generalize this exception. 286 new SplittableAttributeSpec( 287 "measurementSystem-category-temperature", "territories", "type"), 288 new SplittableAttributeSpec("paperSize", "territories", "type"), 289 new SplittableAttributeSpec("parentLocale", "locales", "parent"), 290 new SplittableAttributeSpec( 291 "collations", "locales", "parent"), // parentLocale component=collations 292 new SplittableAttributeSpec( 293 "plurals", "locales", "parent"), // parentLocale component=plurals 294 new SplittableAttributeSpec( 295 "segmentations", "locales", "parent"), // parentLocale component=segmentations 296 new SplittableAttributeSpec("hours", "regions", null), 297 new SplittableAttributeSpec("dayPeriodRules", "locales", null), 298 // new SplittableAttributeSpec("group", "contains", "group"), 299 new SplittableAttributeSpec("personList", "locales", "type"), 300 new SplittableAttributeSpec("unitPreference", "regions", null), 301 new SplittableAttributeSpec("grammaticalFeatures", "locales", null), 302 new SplittableAttributeSpec("grammaticalDerivations", "locales", null), 303 // this will cause EMPTY parentLocales elements to work properly 304 new SplittableAttributeSpec("parentLocales", "component", "" /* Not null */), 305 }; 306 307 /** The set that contains all timezone type of elements. */ 308 public static final Set<String> TIMEZONE_ELEMENT_NAME_SET = 309 Builder.with(new HashSet<String>()) 310 .add("zone") 311 .add("timezone") 312 .add("zoneItem") 313 .add("typeMap") 314 .freeze(); 315 316 /** 317 * There are a handful of attribute values that are more properly represented as an array of 318 * strings rather than as a single string. These are not locked to a specific element, may need 319 * to change the matching algorithm if there are conflicts. 320 */ 321 public static final Set<String> ATTRVALUE_AS_ARRAY_SET = 322 Builder.with(new HashSet<String>()) 323 .add("territories") 324 .add("scripts") 325 .add("contains") 326 .add("systems") 327 .add("origin") 328 .add("component") // for parentLocales - may need to be more specific here 329 .add("localeRules") // for parentLocales 330 .add("values") // for unitIdComponents - may need to be more specific here 331 .freeze(); 332 333 /** 334 * Following is the list of elements that need to be sorted before output. 335 * 336 * <p>Time zone item is split to multiple level, and each level should be grouped together. The 337 * locale list in "dayPeriodRule" could be split to multiple items, and items for each locale 338 * should be grouped together. 339 */ 340 public static final String[] ELEMENT_NEED_SORT = { 341 "zone", 342 "timezone", 343 "zoneItem", 344 "typeMap", 345 "dayPeriodRule", 346 "pluralRanges", 347 "pluralRules", 348 "personList", 349 "calendarPreferenceData", 350 "character-fallback", 351 "types", 352 "timeData", 353 "minDays", 354 "firstDay", 355 "weekendStart", 356 "weekendEnd", 357 "measurementData", 358 "measurementSystem" 359 }; 360 361 /** 362 * Some elements in CLDR has multiple children of the same type of element. We would like to 363 * treat them as array. 364 */ 365 public static final Pattern ARRAY_ITEM_PATTERN = 366 PatternCache.get( 367 "(.*/collation[^/]*/rules[^/]*/" 368 + "|.*/character-fallback[^/]*/character[^/]*/" 369 + "|.*/rbnfrule[^/]*/" 370 + "|.*/ruleset[^/]*/" 371 + "|.*/languageMatching[^/]*/languageMatches[^/]*/" 372 + "|.*/unitPreferences/[^/]*/[^/]*/" 373 + "|.*/windowsZones[^/]*/mapTimezones[^/]*/" 374 + "|.*/metaZones[^/]*/mapTimezones[^/]*/" 375 + "|.*/segmentation[^/]*/variables[^/]*/" 376 + "|.*/segmentation[^/]*/suppressions[^/]*/" 377 + "|.*/transform[^/]*/tRules[^/]*/" 378 + "|.*/region/region[^/]*/" 379 + "|.*/keyword[^/]*/key[^/]*/" 380 + "|.*/telephoneCodeData[^/]*/codesByTerritory[^/]*/" 381 + "|.*/metazoneInfo[^/]*/timezone\\[[^\\]]*\\]/" 382 + "|.*/metadata[^/]*/validity[^/]*/" 383 + "|.*/metadata[^/]*/suppress[^/]*/" 384 + "|.*/metadata[^/]*/deprecated[^/]*/" 385 + ")(.*)"); 386 387 /** These objects values should be output as arrays. */ 388 public static final Pattern VALUE_IS_SPACESEP_ARRAY = 389 PatternCache.get( 390 "(grammaticalCase|grammaticalGender|grammaticalDefiniteness|nameOrderLocales|component)"); 391 392 /** 393 * Indicates that the child value of this element needs to be separated into array items. For 394 * example: {@code <weekOfPreference ordering="weekOfDate weekOfMonth" locales="en bn ja ka"/>} 395 * becomes {@code {"en":["weekOfDate","weekOfMonth"],"bn":["weekOfDate","weekOfMonth"]} } 396 */ 397 public static final Set<String> CHILD_VALUE_IS_SPACESEP_ARRAY = 398 ImmutableSet.of("weekOfPreference", "calendarPreferenceData"); 399 400 /** 401 * Number elements without a numbering system are there only for compatibility purposes. We 402 * automatically suppress generation of JSON objects for them. 403 */ 404 public static final Pattern NO_NUMBERING_SYSTEM_PATTERN = 405 Pattern.compile( 406 "//ldml/numbers/(symbols|(decimal|percent|scientific|currency)Formats)/.*"); 407 408 public static final Pattern NUMBERING_SYSTEM_PATTERN = 409 Pattern.compile( 410 "//ldml/numbers/(symbols|miscPatterns|(decimal|percent|scientific|currency)Formats)\\[@numberSystem=\"([^\"]++)\"\\]/.*"); 411 public static final String[] ACTIVE_NUMBERING_SYSTEM_XPATHS = { 412 "//ldml/numbers/defaultNumberingSystem", 413 "//ldml/numbers/otherNumberingSystems/native", 414 "//ldml/numbers/otherNumberingSystems/traditional", 415 "//ldml/numbers/otherNumberingSystems/finance" 416 }; 417 418 /** 419 * Root language id pattern should be discarded in all locales except root, even though the path 420 * will exist in a resolved CLDRFile. 421 */ 422 public static final Pattern ROOT_IDENTITY_PATTERN = 423 Pattern.compile("//ldml/identity/language\\[@type=\"root\"\\]"); 424 425 /** A simple class to hold the specification of a path transformation. */ 426 public static class PathTransformSpec { 427 428 private final boolean DEBUG_TRANSFORMS = false; 429 public Pattern pattern; 430 public String replacement; 431 public String patternStr; 432 public String comment = ""; 433 private AtomicInteger use = new AtomicInteger(); 434 PathTransformSpec(String patternStr, String replacement, String comment)435 PathTransformSpec(String patternStr, String replacement, String comment) { 436 this.patternStr = patternStr; 437 pattern = PatternCache.get(patternStr); 438 this.replacement = replacement; 439 this.comment = comment; 440 if (this.comment == null) this.comment = ""; 441 } 442 443 @Override toString()444 public String toString() { 445 StringBuilder sb = new StringBuilder(); 446 sb.append('\n') 447 .append("# ") 448 .append(comment.replace('\n', ' ')) 449 .append('\n') 450 .append("< ") 451 .append(patternStr) 452 .append('\n') 453 .append("> ") 454 .append(replacement) 455 .append('\n'); 456 return sb.toString(); 457 } 458 459 /** 460 * Apply this rule to a string 461 * 462 * @param result input string 463 * @return result, or null if unchanged 464 */ apply(String result)465 public String apply(String result) { 466 Matcher m = pattern.matcher(result); 467 if (m.matches()) { 468 final String newResult = m.replaceFirst(replacement); 469 final int count = this.use.incrementAndGet(); 470 if (DEBUG_TRANSFORMS) { 471 System.err.println( 472 result 473 + " => " 474 + newResult 475 + " count " 476 + count 477 + " << " 478 + this.toString()); 479 } 480 return newResult; 481 } 482 return null; 483 } 484 dumpAll()485 public static void dumpAll() { 486 System.out.println("# Path Transformations"); 487 for (final PathTransformSpec ts : getPathTransformations()) { 488 System.out.append(ts.toString()); 489 } 490 System.out.println(); 491 } 492 applyAll(String result)493 public static final String applyAll(String result) { 494 for (final PathTransformSpec ts : getPathTransformations()) { 495 final String changed = ts.apply(result); 496 if (changed != null) { 497 result = changed; 498 break; 499 } 500 } 501 return result; 502 } 503 } 504 getPathTransformations()505 public static final Iterable<PathTransformSpec> getPathTransformations() { 506 return PathTransformSpecHelper.INSTANCE; 507 } 508 509 /** 510 * Add a path transform for the //ldml/identity/version element to the specific number 511 * 512 * @param version 513 */ addVersionHandler(String version)514 public static final void addVersionHandler(String version) { 515 if (!CLDRFile.GEN_VERSION.equals(version)) { 516 PathTransformSpecHelper.INSTANCE.prependVersionTransforms(version); 517 } 518 } 519 520 public static final class PathTransformSpecHelper extends FileProcessor 521 implements Iterable<PathTransformSpec> { 522 static final PathTransformSpecHelper INSTANCE = make(); 523 make()524 static final PathTransformSpecHelper make() { 525 final PathTransformSpecHelper helper = new PathTransformSpecHelper(); 526 helper.process(PathTransformSpecHelper.class, "pathTransforms.txt"); 527 return helper; 528 } 529 PathTransformSpecHelper()530 private PathTransformSpecHelper() {} 531 532 private List<PathTransformSpec> data = new ArrayList<>(); 533 private String lastComment = ""; 534 private String lastPattern = null; 535 private String lastReplacement = null; 536 537 @Override handleStart()538 protected void handleStart() { 539 // Add these to the beginning because of the dynamic version 540 String version = CLDRFile.GEN_VERSION; 541 prependVersionTransforms(version); 542 } 543 544 /** 545 * Prepend version transform. If called twice, the LAST caller will be used. 546 * 547 * @param version 548 */ prependVersionTransforms(String version)549 public void prependVersionTransforms(String version) { 550 data.add( 551 0, 552 new PathTransformSpec( 553 "(.+)/identity/version\\[@number=\"([^\"]*)\"\\]", 554 "$1" + "/identity/version\\[@cldrVersion=\"" + version + "\"\\]", 555 "added by code")); 556 // Add cldrVersion attribute to supplemental data 557 data.add( 558 0, 559 new PathTransformSpec( 560 "(.+)/version\\[@number=\"([^\"]*)\"\\]\\[@unicodeVersion=\"([^\"]*\")(\\])", 561 "$1" 562 + "/version\\[@cldrVersion=\"" 563 + version 564 + "\"\\]" 565 + "\\[@unicodeVersion=\"" 566 + "$3" 567 + "\\]", 568 "added by code")); 569 } 570 571 @Override handleLine(int lineCount, String line)572 protected boolean handleLine(int lineCount, String line) { 573 if (line.isEmpty()) return true; 574 if (line.startsWith("<")) { 575 lastReplacement = null; 576 if (lastPattern != null) { 577 throw new IllegalArgumentException("line " + lineCount + ": two <'s in a row"); 578 } 579 lastPattern = line.substring(1).trim(); 580 if (lastPattern.isEmpty()) { 581 throw new IllegalArgumentException("line " + lineCount + ": empty < pattern"); 582 } 583 } else if (line.startsWith(">")) { 584 if (lastPattern == null) { 585 throw new IllegalArgumentException( 586 "line " + lineCount + ": need < line before > line"); 587 } 588 lastReplacement = line.substring(1).trim(); 589 data.add(new PathTransformSpec(lastPattern, lastReplacement, lastComment)); 590 reset(); 591 } 592 return true; 593 } 594 595 @Override handleEnd()596 protected void handleEnd() { 597 if (lastPattern != null) { 598 throw new IllegalArgumentException("ended with a < but no >"); 599 } 600 } 601 reset()602 private void reset() { 603 this.lastComment = ""; 604 this.lastPattern = null; 605 this.lastReplacement = null; 606 } 607 608 @Override handleComment(String line, int commentCharPosition)609 public void handleComment(String line, int commentCharPosition) { 610 lastComment = line.substring(commentCharPosition + 1).trim(); 611 } 612 613 @Override iterator()614 public Iterator<PathTransformSpec> iterator() { 615 return data.iterator(); 616 } 617 } 618 main(String args[])619 public static void main(String args[]) { 620 // for debugging / verification 621 PathTransformSpec.dumpAll(); 622 } 623 getKeyStr(String name, String key)624 public static final String getKeyStr(String name, String key) { 625 String keyStr2 = "*:" + name + ":" + key; 626 return keyStr2; 627 } 628 getKeyStr(String parent, String name, String key)629 public static final String getKeyStr(String parent, String name, String key) { 630 String keyStr = parent + ":" + name + ":" + key; 631 return keyStr; 632 } 633 getSplittableAttrs()634 public static SplittableAttributeSpec[] getSplittableAttrs() { 635 return SPLITTABLE_ATTRS; 636 } 637 valueIsSpacesepArray(final String nodeName, String parent)638 public static final boolean valueIsSpacesepArray(final String nodeName, String parent) { 639 return VALUE_IS_SPACESEP_ARRAY.matcher(nodeName).matches() 640 || (parent != null && CHILD_VALUE_IS_SPACESEP_ARRAY.contains(parent)); 641 } 642 643 static final Set<String> BOOLEAN_OMIT_FALSE = 644 ImmutableSet.of( 645 // attribute names within bcp47 that are booleans, but omitted if false. 646 "deprecated"); 647 648 // These attributes are booleans, and should be omitted if false attrIsBooleanOmitFalse( final String fullPath, final String nodeName, final String parent, final String key)649 public static final boolean attrIsBooleanOmitFalse( 650 final String fullPath, final String nodeName, final String parent, final String key) { 651 return (fullPath != null 652 && (fullPath.startsWith("//supplementalData/metaZones/metazoneIds") 653 || fullPath.startsWith("//ldmlBCP47/keyword/key")) 654 && BOOLEAN_OMIT_FALSE.contains(key)); 655 } 656 } 657