• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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                     // transforms
160 
161                     // transforms
162                     "transforms:transform:source",
163                     "transforms:transform:target",
164                     "transforms:transform:direction",
165                     "transforms:transform:variant");
166 
167     /**
168      * The set of element:attribute pair in which the attribute should be treated as value. All the
169      * attribute here are non-distinguishing attributes.
170      */
171 
172     /**
173      * For those attributes that are treated as values, they taken the form of element_name: { ...,
174      * attribute: value, ...} This is desirable as an element may have several attributes that are
175      * treated as values. But in some cases, there is one such attribute only, and it is more
176      * desirable to convert element_name: { attribute: value} to element_name: value With a solid
177      * example, (likelySubtags:likelySubtag:to) <likelySubtag from="zh" to="zh_Hans_CN" />
178      * distinguishing attr "from" will become the key, its better to omit "to" and have this simple
179      * mapping: "zh" : "zh_Hans_CN",
180      */
181     static final ImmutableSet<String> COMPACTABLE_ATTR_AS_VALUE_SET =
182             ImmutableSet.of(
183                     // parent:element:attribute
184                     // common/main
185                     "calendars:default:choice",
186                     "dateFormats:default:choice",
187                     "months:default:choice",
188                     "monthContext:default:choice",
189                     "days:default:choice",
190                     "dayContext:default:choice",
191                     "timeFormats:default:choice",
192                     "dateTimeFormats:default:choice",
193                     "timeZoneNames:singleCountries:list",
194 
195                     // rbnf
196                     "ruleset:rbnfrule:value",
197                     // common/supplemental
198                     "likelySubtags:likelySubtag:to",
199                     // "territoryContainment:group:type",
200                     "calendar:calendarSystem:type",
201                     "calendarPreferenceData:calendarPreference:ordering",
202                     "codesByTerritory:telephoneCountryCode:code",
203 
204                     // common/collation
205                     "collations:default:choice",
206 
207                     // common/supplemental/pluralRanges.xml
208                     "pluralRanges:pluralRange:result",
209 
210                     // identity elements
211                     "identity:language:type",
212                     "identity:script:type",
213                     "identity:territory:type",
214                     "identity:variant:type",
215                     "grammaticalFeatures:grammaticalGender:values",
216                     "grammaticalFeatures:grammaticalDefiniteness:values",
217                     "grammaticalFeatures:grammaticalCase:values",
218                     "grammaticalDerivations:deriveCompound:value");
219 
220     /**
221      * The set of attributes that should be treated as value, and reduce to simple value only form.
222      */
223 
224     /** Anonymous key name. */
225     public static final String ANONYMOUS_KEY = "_";
226 
227     /**
228      * Check if the attribute should be suppressed.
229      *
230      * <p>Right now only "_q" is suppressed. In most cases array is used and there is no need for
231      * this information. In other cases, order is irrelevant.
232      *
233      * @return True if the attribute should be suppressed.
234      */
IsSuppresedAttr(String attr)235     public static boolean IsSuppresedAttr(String attr) {
236         return attr.endsWith("_q") || attr.endsWith("-q");
237     }
238 
239     /** The set of attributes that should be ignored in the conversion process. */
240     public static final ImmutableSet<String> IGNORABLE_NONDISTINGUISHING_ATTR_SET =
241             ImmutableSet.of("draft", "references", "origin");
242 
243     /**
244      * List of attributes that should be suppressed. This list comes from
245      * cldr/common/supplemental/supplementalMetadata. Each three of them is a group, they are for
246      * element, value and attribute. If the specified attribute appears in specified element with
247      * specified = value, it should be suppressed.
248      */
249     public static final String[] ATTR_SUPPRESS_LIST = {
250         // common/main
251         "dateFormat", "standard", "type",
252         "dateTimeFormat", "standard", "type",
253         "timeFormat", "standard", "type",
254         "decimalFormat", "standard", "type",
255         "percentFormat", "standard", "type",
256         "scientificFormat", "standard", "type",
257         "pattern", "standard", "type"
258     };
259 
260     /** This is a simple class to hold the splittable attribute specification. */
261     public static class SplittableAttributeSpec {
262         public String element;
263         public String attribute;
264         public String attrAsValueAfterSplit;
265 
SplittableAttributeSpec(String el, String attr, String av)266         SplittableAttributeSpec(String el, String attr, String av) {
267             element = el;
268             attribute = attr;
269             attrAsValueAfterSplit = av;
270         }
271     }
272 
273     /**
274      * List of attributes that has value that can be split. Each two of them is a group, and
275      * represent element and value. Occurrences of such match should lead to creation of multiple
276      * node. Example: <weekendStart day="thu" territories="DZ KW OM SA SD YE AF IR"/> should be
277      * treated as if following node is encountered. <weekendStart day="thu" territories="DZ"/>
278      * <weekendStart day="thu" territories="KW"/> <weekendStart day="thu" territories="OM"/>
279      * <weekendStart day="thu" territories="SA"/> <weekendStart day="thu" territories="SD"/>
280      * <weekendStart day="thu" territories="YE"/> <weekendStart day="thu" territories="AF"/>
281      * <weekendStart day="thu" territories="IR"/>
282      */
283     private static final SplittableAttributeSpec[] SPLITTABLE_ATTRS = {
284         new SplittableAttributeSpec("calendarPreference", "territories", null),
285         new SplittableAttributeSpec("pluralRanges", "locales", null),
286         new SplittableAttributeSpec("pluralRules", "locales", null),
287         new SplittableAttributeSpec("minDays", "territories", "count"),
288         new SplittableAttributeSpec("firstDay", "territories", "day"),
289         new SplittableAttributeSpec("weekendStart", "territories", "day"),
290         new SplittableAttributeSpec("weekendEnd", "territories", "day"),
291         new SplittableAttributeSpec("weekOfPreference", "locales", "ordering"),
292         new SplittableAttributeSpec("measurementSystem", "territories", "type"),
293         // this is deprecated, so no need to generalize this exception.
294         new SplittableAttributeSpec(
295                 "measurementSystem-category-temperature", "territories", "type"),
296         new SplittableAttributeSpec("paperSize", "territories", "type"),
297         new SplittableAttributeSpec("parentLocale", "locales", "parent"),
298         new SplittableAttributeSpec(
299                 "collations", "locales", "parent"), // parentLocale component=collations
300         new SplittableAttributeSpec(
301                 "plurals", "locales", "parent"), // parentLocale component=plurals
302         new SplittableAttributeSpec(
303                 "segmentations", "locales", "parent"), // parentLocale component=segmentations
304         new SplittableAttributeSpec("hours", "regions", null),
305         new SplittableAttributeSpec("dayPeriodRules", "locales", null),
306         // new SplittableAttributeSpec("group", "contains", "group"),
307         new SplittableAttributeSpec("personList", "locales", "type"),
308         new SplittableAttributeSpec("unitPreference", "regions", null),
309         new SplittableAttributeSpec("grammaticalFeatures", "locales", null),
310         new SplittableAttributeSpec("grammaticalDerivations", "locales", null),
311         // this will cause EMPTY parentLocales elements to work properly
312         new SplittableAttributeSpec("parentLocales", "component", "" /* Not null */),
313     };
314 
315     /** The set that contains all timezone type of elements. */
316     public static final Set<String> TIMEZONE_ELEMENT_NAME_SET =
317             Builder.with(new HashSet<String>())
318                     .add("zone")
319                     .add("timezone")
320                     .add("zoneItem")
321                     .add("typeMap")
322                     .freeze();
323 
324     /**
325      * There are a handful of attribute values that are more properly represented as an array of
326      * strings rather than as a single string. These are not locked to a specific element, may need
327      * to change the matching algorithm if there are conflicts.
328      */
329     public static final Set<String> ATTRVALUE_AS_ARRAY_SET =
330             Builder.with(new HashSet<String>())
331                     .add("territories")
332                     .add("scripts")
333                     .add("contains")
334                     .add("systems")
335                     .add("origin")
336                     .add("component") // for parentLocales - may need to be more specific here
337                     .add("localeRules") // for parentLocales
338                     .add("values") // for unitIdComponents - may need to be more specific here
339                     .freeze();
340 
341     /**
342      * Following is the list of elements that need to be sorted before output.
343      *
344      * <p>Time zone item is split to multiple level, and each level should be grouped together. The
345      * locale list in "dayPeriodRule" could be split to multiple items, and items for each locale
346      * should be grouped together.
347      */
348     public static final String[] ELEMENT_NEED_SORT = {
349         "zone",
350         "timezone",
351         "zoneItem",
352         "typeMap",
353         "dayPeriodRule",
354         "pluralRanges",
355         "pluralRules",
356         "personList",
357         "calendarPreferenceData",
358         "character-fallback",
359         "types",
360         "timeData",
361         "minDays",
362         "firstDay",
363         "weekendStart",
364         "weekendEnd",
365         "measurementData",
366         "measurementSystem"
367     };
368 
369     /**
370      * Some elements in CLDR has multiple children of the same type of element. We would like to
371      * treat them as array.
372      */
373     public static final Pattern ARRAY_ITEM_PATTERN =
374             PatternCache.get(
375                     "(.*/collation[^/]*/rules[^/]*/"
376                             + "|.*/character-fallback[^/]*/character[^/]*/"
377                             + "|.*/rbnfrule[^/]*/"
378                             + "|.*/ruleset[^/]*/"
379                             + "|.*/languageMatching[^/]*/languageMatches[^/]*/"
380                             + "|.*/unitPreferences/[^/]*/[^/]*/"
381                             + "|.*/windowsZones[^/]*/mapTimezones[^/]*/"
382                             + "|.*/metaZones[^/]*/mapTimezones[^/]*/"
383                             + "|.*/segmentation[^/]*/variables[^/]*/"
384                             + "|.*/segmentation[^/]*/suppressions[^/]*/"
385                             + "|.*/transform[^/]*/tRules[^/]*/"
386                             + "|.*/region/region[^/]*/"
387                             + "|.*/keyword[^/]*/key[^/]*/"
388                             + "|.*/telephoneCodeData[^/]*/codesByTerritory[^/]*/"
389                             + "|.*/metazoneInfo[^/]*/timezone\\[[^\\]]*\\]/"
390                             + "|.*/metadata[^/]*/validity[^/]*/"
391                             + "|.*/metadata[^/]*/suppress[^/]*/"
392                             + "|.*/metadata[^/]*/deprecated[^/]*/"
393                             + ")(.*)");
394 
395     /** These objects values should be output as arrays. */
396     public static final Pattern VALUE_IS_SPACESEP_ARRAY =
397             PatternCache.get(
398                     "(grammaticalCase|grammaticalGender|grammaticalDefiniteness|nameOrderLocales|component)");
399 
400     /**
401      * Indicates that the child value of this element needs to be separated into array items. For
402      * example: {@code <weekOfPreference ordering="weekOfDate weekOfMonth" locales="en bn ja ka"/>}
403      * becomes {@code {"en":["weekOfDate","weekOfMonth"],"bn":["weekOfDate","weekOfMonth"]} }
404      */
405     public static final Set<String> CHILD_VALUE_IS_SPACESEP_ARRAY =
406             ImmutableSet.of("weekOfPreference", "calendarPreferenceData");
407 
408     /**
409      * Number elements without a numbering system are there only for compatibility purposes. We
410      * automatically suppress generation of JSON objects for them.
411      */
412     public static final Pattern NO_NUMBERING_SYSTEM_PATTERN =
413             Pattern.compile(
414                     "//ldml/numbers/(symbols|(decimal|percent|scientific|currency)Formats)/.*");
415 
416     public static final Pattern NUMBERING_SYSTEM_PATTERN =
417             Pattern.compile(
418                     "//ldml/numbers/(symbols|miscPatterns|(decimal|percent|scientific|currency)Formats)\\[@numberSystem=\"([^\"]++)\"\\]/.*");
419     public static final String[] ACTIVE_NUMBERING_SYSTEM_XPATHS = {
420         "//ldml/numbers/defaultNumberingSystem",
421         "//ldml/numbers/otherNumberingSystems/native",
422         "//ldml/numbers/otherNumberingSystems/traditional",
423         "//ldml/numbers/otherNumberingSystems/finance"
424     };
425 
426     /**
427      * Root language id pattern should be discarded in all locales except root, even though the path
428      * will exist in a resolved CLDRFile.
429      */
430     public static final Pattern ROOT_IDENTITY_PATTERN =
431             Pattern.compile("//ldml/identity/language\\[@type=\"root\"\\]");
432 
433     /**
434      * Version (coming from DTD) should be discarded everywhere. This information is now in
435      * package.json.
436      */
437     public static final Pattern VERSION_PATTERN = Pattern.compile("//ldml/identity/version.*");
438 
439     /** A simple class to hold the specification of a path transformation. */
440     public static class PathTransformSpec {
441 
442         private final boolean DEBUG_TRANSFORMS = false;
443         public Pattern pattern;
444         public String replacement;
445         public String patternStr;
446         public String comment = "";
447         private AtomicInteger use = new AtomicInteger();
448 
PathTransformSpec(String patternStr, String replacement, String comment)449         PathTransformSpec(String patternStr, String replacement, String comment) {
450             this.patternStr = patternStr;
451             pattern = PatternCache.get(patternStr);
452             this.replacement = replacement;
453             this.comment = comment;
454             if (this.comment == null) this.comment = "";
455         }
456 
457         @Override
toString()458         public String toString() {
459             StringBuilder sb = new StringBuilder();
460             sb.append('\n')
461                     .append("# ")
462                     .append(comment.replace('\n', ' '))
463                     .append('\n')
464                     .append("< ")
465                     .append(patternStr)
466                     .append('\n')
467                     .append("> ")
468                     .append(replacement)
469                     .append('\n');
470             return sb.toString();
471         }
472 
473         /**
474          * Apply this rule to a string
475          *
476          * @param result input string
477          * @return result, or null if unchanged
478          */
apply(String result)479         public String apply(String result) {
480             Matcher m = pattern.matcher(result);
481             if (m.matches()) {
482                 final String newResult = m.replaceFirst(replacement);
483                 final int count = this.use.incrementAndGet();
484                 if (DEBUG_TRANSFORMS) {
485                     System.err.println(
486                             result
487                                     + " => "
488                                     + newResult
489                                     + " count "
490                                     + count
491                                     + " << "
492                                     + this.toString());
493                 }
494                 return newResult;
495             }
496             return null;
497         }
498 
dumpAll()499         public static void dumpAll() {
500             System.out.println("# Path Transformations");
501             for (final PathTransformSpec ts : getPathTransformations()) {
502                 System.out.append(ts.toString());
503             }
504             System.out.println();
505         }
506 
applyAll(String result)507         public static final String applyAll(String result) {
508             for (final PathTransformSpec ts : getPathTransformations()) {
509                 final String changed = ts.apply(result);
510                 if (changed != null) {
511                     result = changed;
512                     break;
513                 }
514             }
515             return result;
516         }
517     }
518 
getPathTransformations()519     public static final Iterable<PathTransformSpec> getPathTransformations() {
520         return PathTransformSpecHelper.INSTANCE;
521     }
522 
523     /**
524      * Add a path transform for the //ldml/identity/version element to the specific number
525      *
526      * @param version
527      */
addVersionHandler(String version)528     public static final void addVersionHandler(String version) {
529         if (!CLDRFile.GEN_VERSION.equals(version)) {
530             PathTransformSpecHelper.INSTANCE.prependVersionTransforms(version);
531         }
532     }
533 
534     public static final class PathTransformSpecHelper extends FileProcessor
535             implements Iterable<PathTransformSpec> {
536         static final PathTransformSpecHelper INSTANCE = make();
537 
make()538         static final PathTransformSpecHelper make() {
539             final PathTransformSpecHelper helper = new PathTransformSpecHelper();
540             helper.process(PathTransformSpecHelper.class, "pathTransforms.txt");
541             return helper;
542         }
543 
PathTransformSpecHelper()544         private PathTransformSpecHelper() {}
545 
546         private List<PathTransformSpec> data = new ArrayList<>();
547         private String lastComment = "";
548         private String lastPattern = null;
549         private String lastReplacement = null;
550 
551         @Override
handleStart()552         protected void handleStart() {
553             // Add these to the beginning because of the dynamic version
554             String version = CLDRFile.GEN_VERSION;
555             prependVersionTransforms(version);
556         }
557 
558         /**
559          * Prepend version transform. If called twice, the LAST caller will be used.
560          *
561          * @param version
562          */
prependVersionTransforms(String version)563         public void prependVersionTransforms(String version) {
564             data.add(
565                     0,
566                     new PathTransformSpec(
567                             "(.+)/identity/version\\[@number=\"([^\"]*)\"\\]",
568                             "$1" + "/identity/version\\[@cldrVersion=\"" + version + "\"\\]",
569                             "added by code"));
570             // Add cldrVersion attribute to supplemental data
571             data.add(
572                     0,
573                     new PathTransformSpec(
574                             "(.+)/version\\[@number=\"([^\"]*)\"\\]\\[@unicodeVersion=\"([^\"]*\")(\\])",
575                             "$1"
576                                     + "/version\\[@cldrVersion=\""
577                                     + version
578                                     + "\"\\]"
579                                     + "\\[@unicodeVersion=\""
580                                     + "$3"
581                                     + "\\]",
582                             "added by code"));
583         }
584 
585         @Override
handleLine(int lineCount, String line)586         protected boolean handleLine(int lineCount, String line) {
587             if (line.isEmpty()) return true;
588             if (line.startsWith("<")) {
589                 lastReplacement = null;
590                 if (lastPattern != null) {
591                     throw new IllegalArgumentException("line " + lineCount + ": two <'s in a row");
592                 }
593                 lastPattern = line.substring(1).trim();
594                 if (lastPattern.isEmpty()) {
595                     throw new IllegalArgumentException("line " + lineCount + ": empty < pattern");
596                 }
597             } else if (line.startsWith(">")) {
598                 if (lastPattern == null) {
599                     throw new IllegalArgumentException(
600                             "line " + lineCount + ": need < line before > line");
601                 }
602                 lastReplacement = line.substring(1).trim();
603                 data.add(new PathTransformSpec(lastPattern, lastReplacement, lastComment));
604                 reset();
605             }
606             return true;
607         }
608 
609         @Override
handleEnd()610         protected void handleEnd() {
611             if (lastPattern != null) {
612                 throw new IllegalArgumentException("ended with a < but no >");
613             }
614         }
615 
reset()616         private void reset() {
617             this.lastComment = "";
618             this.lastPattern = null;
619             this.lastReplacement = null;
620         }
621 
622         @Override
handleComment(String line, int commentCharPosition)623         public void handleComment(String line, int commentCharPosition) {
624             lastComment = line.substring(commentCharPosition + 1).trim();
625         }
626 
627         @Override
iterator()628         public Iterator<PathTransformSpec> iterator() {
629             return data.iterator();
630         }
631     }
632 
main(String args[])633     public static void main(String args[]) {
634         // for debugging / verification
635         PathTransformSpec.dumpAll();
636     }
637 
getKeyStr(String name, String key)638     public static final String getKeyStr(String name, String key) {
639         String keyStr2 = "*:" + name + ":" + key;
640         return keyStr2;
641     }
642 
getKeyStr(String parent, String name, String key)643     public static final String getKeyStr(String parent, String name, String key) {
644         String keyStr = parent + ":" + name + ":" + key;
645         return keyStr;
646     }
647 
getSplittableAttrs()648     public static SplittableAttributeSpec[] getSplittableAttrs() {
649         return SPLITTABLE_ATTRS;
650     }
651 
valueIsSpacesepArray(final String nodeName, String parent)652     public static final boolean valueIsSpacesepArray(final String nodeName, String parent) {
653         return VALUE_IS_SPACESEP_ARRAY.matcher(nodeName).matches()
654                 || (parent != null && CHILD_VALUE_IS_SPACESEP_ARRAY.contains(parent));
655     }
656 
657     static final Set<String> BOOLEAN_OMIT_FALSE =
658             ImmutableSet.of(
659                     // attribute names within bcp47 that are booleans, but omitted if false.
660                     "deprecated");
661 
662     // These attributes are booleans, and should be omitted if false
attrIsBooleanOmitFalse( final String fullPath, final String nodeName, final String parent, final String key)663     public static final boolean attrIsBooleanOmitFalse(
664             final String fullPath, final String nodeName, final String parent, final String key) {
665         return (fullPath != null
666                 && (fullPath.startsWith("//supplementalData/metaZones/metazoneIds")
667                         || fullPath.startsWith("//ldmlBCP47/keyword/key"))
668                 && BOOLEAN_OMIT_FALSE.contains(key));
669     }
670 }
671