• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.unicode.cldr.icu;
2 
3 import java.io.BufferedReader;
4 import java.util.ArrayList;
5 import java.util.Arrays;
6 import java.util.Collections;
7 import java.util.Comparator;
8 import java.util.HashMap;
9 import java.util.HashSet;
10 import java.util.Iterator;
11 import java.util.List;
12 import java.util.Map;
13 import java.util.Map.Entry;
14 import java.util.Set;
15 import java.util.regex.Matcher;
16 import java.util.regex.Pattern;
17 
18 import org.unicode.cldr.util.CLDRFile;
19 import org.unicode.cldr.util.CldrUtility.VariableReplacer;
20 import org.unicode.cldr.util.FileReaders;
21 import org.unicode.cldr.util.PatternCache;
22 import org.unicode.cldr.util.RegexLookup;
23 import org.unicode.cldr.util.RegexLookup.Finder;
24 import org.unicode.cldr.util.RegexLookup.Merger;
25 import org.unicode.cldr.util.RegexLookup.RegexFinder;
26 
27 import com.ibm.icu.impl.Row.R2;
28 import com.ibm.icu.impl.Row.R3;
29 import com.ibm.icu.text.Transform;
30 import com.ibm.icu.text.UnicodeSet;
31 import com.ibm.icu.util.Output;
32 
33 /**
34  * A class for loading and managing the xpath converters used to map CLDR data
35  * to ICU.
36  * @author jchye
37  */
38 class RegexManager {
39     private static final Pattern SEMI = PatternCache.get("\\s*+;\\s*+");
40     private static final Pattern FUNCTION_PATTERN = PatternCache.get("\\&(\\w++)\\(([^\\)]++)\\)");
41     private static final Pattern QUOTES = PatternCache.get("\"([^\"]++)\"");
42     private static final UnicodeSet SPACE_CHARACTERS = new UnicodeSet(
43         "[\\u0000\\uFEFF[:pattern_whitespace:]]");
44     // Matches arguments with or without enclosing quotes.
45     private static final Pattern ARGUMENT = PatternCache.get("[<\"]?\\$(\\d)[\">]?");
46 
47     private static Map<String, Function> functionMap = new HashMap<String, Function>();
48 
49     private String converterFile;
50     private Map<String, RegexResult> unprocessedMatchers;
51     private Map<String, String> xpathVariables;
52     private VariableReplacer cldrVariables;
53 
54     /**
55      * Wrapper class for functions that need to be performed on CLDR values as
56      * part of the ICU conversion,
57      */
58     static abstract class Function {
59         int maxArgs;
60 
61         /**
62          * @param maxArgs
63          *            the maximum number of args that can be passed to this function,
64          *            or a negative number if there is no limit.
65          */
Function(int maxArgs)66         Function(int maxArgs) {
67             this.maxArgs = maxArgs;
68         }
69 
70         /**
71          * Processes a comma-delimited list of arguments.
72          *
73          * @param arg
74          * @return
75          */
process(String arg)76         public String process(String arg) {
77             String[] args = arg.split(",");
78             if (maxArgs > -1 && args.length > maxArgs) {
79                 throw new IllegalArgumentException("Function has too many args: expected "
80                     + maxArgs + " but got (" + arg + ")");
81             }
82             return run(args);
83         }
84 
85         /**
86          * Runs the function on a list of arguments.
87          *
88          * @return the resultant string
89          */
run(String... args)90         abstract String run(String... args);
91     }
92 
93     /**
94      * A wrapper class for storing and working with the unprocessed path-value pairs of a RegexResult.
95      */
96     static class PathValueInfo {
97         private String rbPath;
98         private String valueArg;
99         private String groupKey;
100         private String baseXPath;
101         private int splitRbPathArg;
102 
103         /**
104          * @param rbPath
105          *            the rbPath expression to be used for this set of path-value pairs.
106          * @param instructions
107          *            any special instructions to be carried out on this set of path-value pairs
108          * @param splitRbPathArg
109          *            if applicable, the number of the argument in the rbPath
110          *            expression that needs to be split to create multiple rbPaths, each with the same value
111          */
PathValueInfo(String rbPath, Map<String, String> instructions, int splitRbPathArg)112         public PathValueInfo(String rbPath, Map<String, String> instructions, int splitRbPathArg) {
113             this.rbPath = rbPath;
114             this.valueArg = instructions.get("values");
115             this.groupKey = instructions.get("group");
116             this.baseXPath = instructions.get("base_xpath");
117             this.splitRbPathArg = splitRbPathArg;
118         }
119 
120         /**
121          * @param arguments
122          *            the arguments retrieved from the regex corresponding to this PathValueInfo
123          * @return the processed rb path
124          */
processRbPath(String[] arguments)125         public String processRbPath(String[] arguments) {
126             String path = processString(rbPath, arguments);
127             // Replace slashes in metazone names,
128             // e.g. "America/Argentina/La_Rioja"
129             Matcher matcher = QUOTES.matcher(path);
130             if (matcher.find()) {
131                 path = path.substring(0, matcher.start(1))
132                     + matcher.group(1).replace('/', ':')
133                     + path.substring(matcher.end(1));
134             }
135             return path;
136         }
137 
processGroupKey(String[] arguments)138         public String processGroupKey(String[] arguments) {
139             return processString(groupKey, arguments);
140         }
141 
processValues(String[] arguments, String cldrValue)142         public List<String> processValues(String[] arguments, String cldrValue) {
143             if (valueArg == null) {
144                 List<String> values = new ArrayList<String>();
145                 values.add(cldrValue);
146                 return values;
147             }
148             // Split single args using spaces.
149             String processedValue = processValue(valueArg, cldrValue, arguments);
150             return splitValues(processedValue);
151         }
152 
processXPath(String[] arguments, String xpath)153         public String processXPath(String[] arguments, String xpath) {
154             if (baseXPath != null) {
155                 return processString(baseXPath, arguments);
156             } else {
157                 return xpath;
158             }
159         }
160 
splitValues(String unsplitValues)161         private List<String> splitValues(String unsplitValues) {
162             // Split up values using spaces unless enclosed by non-escaped quotes,
163             // e.g. a "b \" c" would become {"a", "b \" c"}
164             List<String> args = new ArrayList<String>();
165             StringBuffer valueBuffer = new StringBuffer();
166             boolean shouldSplit = true;
167             char lastChar = ' ';
168             for (char c : unsplitValues.toCharArray()) {
169                 // Normalize whitespace input.
170                 if (SPACE_CHARACTERS.contains(c)) {
171                     c = ' ';
172                 }
173                 if (c == '"' && lastChar != '\\') {
174                     shouldSplit = !shouldSplit;
175                 } else if (c == ' ') {
176                     if (lastChar == ' ') {
177                         // Do nothing.
178                     } else if (shouldSplit) {
179                         args.add(valueBuffer.toString());
180                         valueBuffer.setLength(0);
181                     } else {
182                         valueBuffer.append(c);
183                     }
184                 } else {
185                     valueBuffer.append(c);
186                 }
187                 lastChar = c;
188             }
189             if (valueBuffer.length() > 0) {
190                 args.add(valueBuffer.toString());
191             }
192             return args;
193         }
194 
processValue(String value, String cldrValue, String[] arguments)195         private String processValue(String value, String cldrValue, String[] arguments) {
196             value = processString(value, arguments);
197             Matcher matcher = FUNCTION_PATTERN.matcher(value);
198             if (matcher.find()) {
199                 int index = 0;
200                 StringBuffer buffer = new StringBuffer();
201                 do {
202                     buffer.append(value.substring(index, matcher.start()));
203                     String fnName = matcher.group(1);
204                     String fnArgs = matcher.group(2);
205                     String result = functionMap.get(fnName).process(fnArgs);
206                     buffer.append(result);
207                     index = matcher.end();
208                 } while (matcher.find());
209                 buffer.append(value.substring(index));
210                 // System.out.println(value + " to " + buffer.toString());
211                 value = buffer.toString();
212             }
213             if (value.contains("{value}")) {
214                 value = value.replace("{value}", cldrValue);
215             }
216             return value;
217         }
218 
219         @Override
toString()220         public String toString() {
221             return rbPath + "=" + valueArg;
222         }
223 
getSplitRbPathArg()224         public int getSplitRbPathArg() {
225             return splitRbPathArg;
226         }
227 
228         @Override
equals(Object o)229         public boolean equals(Object o) {
230             if (o instanceof PathValueInfo) {
231                 PathValueInfo otherInfo = (PathValueInfo) o;
232                 return rbPath.equals(otherInfo.rbPath)
233                     && valueArg.equals(otherInfo.valueArg);
234             } else {
235                 return false;
236             }
237         }
238     }
239 
processString(String value, String[] arguments)240     static String processString(String value, String[] arguments) {
241         if (value == null) {
242             return null;
243         }
244         try {
245             return RegexLookup.replace(value, arguments);
246         } catch (ArrayIndexOutOfBoundsException e) {
247             throw new RuntimeException("Error while filling out arguments in " + value + " with "
248                 + Arrays.asList(arguments), e);
249         }
250     }
251 
252     static class RegexResult implements Iterable<PathValueInfo> {
253 
254         private Set<PathValueInfo> unprocessed;
255 
RegexResult()256         public RegexResult() {
257             unprocessed = new HashSet<PathValueInfo>();
258         }
259 
add(String rbPath, Map<String, String> instructions, int splitRbPathArg)260         public void add(String rbPath, Map<String, String> instructions,
261             int splitRbPathArg) {
262             unprocessed.add(new PathValueInfo(rbPath, instructions, splitRbPathArg));
263         }
264 
265         @Override
iterator()266         public Iterator<PathValueInfo> iterator() {
267             return unprocessed.iterator();
268         }
269     }
270 
271     static class CldrArray {
272         // Map of xpaths to values and group key.
273         private Map<String, R2<List<String>, String>> map;
274 
CldrArray()275         public CldrArray() {
276             map = new HashMap<String, R2<List<String>, String>>();
277         }
278 
put(String key, List<String> values, String groupKey)279         public void put(String key, List<String> values, String groupKey) {
280             map.put(key, new R2<List<String>, String>(values, groupKey));
281         }
282 
add(String key, String[] values, String groupKey)283         public void add(String key, String[] values, String groupKey) {
284             List<String> list = new ArrayList<String>();
285             for (String value : values) {
286                 list.add(value);
287             }
288             put(key, list, groupKey);
289         }
290 
add(String key, String value, String groupKey)291         public void add(String key, String value, String groupKey) {
292             List<String> list = new ArrayList<String>();
293             list.add(value);
294             put(key, list, groupKey);
295         }
296 
addAll(CldrArray otherArray)297         public void addAll(CldrArray otherArray) {
298             // HACK: narrow alias to abbreviated. Remove after CLDR data fixed.
299             for (String otherKey : otherArray.map.keySet()) {
300                 String narrowPath = otherKey.replace("eraAbbr", "eraNarrow");
301                 if (!map.containsKey(narrowPath)) {
302                     map.put(narrowPath, otherArray.map.get(otherKey));
303                 }
304 
305             }
306         }
307 
findKey(Finder finder)308         public boolean findKey(Finder finder) {
309             for (String key : map.keySet()) {
310                 if (finder.find(key, null, null)) {
311                     return true;
312                 }
313             }
314             return false;
315         }
316 
sortValues(Comparator<String> comparator)317         public List<String[]> sortValues(Comparator<String> comparator) {
318             List<String> sortedKeys = new ArrayList<String>(map.keySet());
319             Collections.sort(sortedKeys, comparator);
320             List<String[]> sortedValues = new ArrayList<String[]>();
321             // Group isArray for the same xpath together.
322             List<String> arrayValues = new ArrayList<String>();
323             for (int i = 0, len = sortedKeys.size(); i < len; i++) {
324                 String key = sortedKeys.get(i);
325                 R2<List<String>, String> currentEntry = map.get(key);
326                 List<String> values = currentEntry.get0();
327                 String groupKey = currentEntry.get1();
328                 if (groupKey == null) {
329                     for (String value : values) {
330                         sortedValues.add(new String[] { value });
331                     }
332                 } else {
333                     arrayValues.addAll(values);
334                     String nextKey = null;
335                     if (i < len - 1) {
336                         nextKey = map.get(sortedKeys.get(i + 1)).get1();
337                     }
338                     if (!groupKey.equals(nextKey)) {
339                         sortedValues.add(toArray(arrayValues));
340                         arrayValues.clear();
341                     }
342                 }
343             }
344             return sortedValues;
345         }
346     }
347 
348     /**
349      * Converts a list into an Array.
350      */
toArray(List<String> list)351     static String[] toArray(List<String> list) {
352         String[] array = new String[list.size()];
353         list.toArray(array);
354         return array;
355     }
356 
357     private static Transform<String, Finder> regexTransform = new Transform<String, Finder>() {
358         @Override
359         public Finder transform(String source) {
360             return new RegexFinder(source);
361         }
362     };
363 
addFunction(String fnName, Function function)364     Map<String, Function> addFunction(String fnName, Function function) {
365         functionMap.put(fnName, function);
366         return functionMap;
367     }
368 
369     /**
370      * Checks if two strings match a specified pattern.
371      *
372      * @param pattern
373      *            the pattern to be matched against
374      * @param arg0
375      * @param arg1
376      * @param matchers
377      *            a 2-element array to contain the two matchers. The
378      *            array is not guaranteed to be filled if matches() returns false
379      * @return true if both strings successfully matched the pattern
380      */
matches(Pattern pattern, String arg0, String arg1, Matcher[] matchers)381     static boolean matches(Pattern pattern, String arg0, String arg1, Matcher[] matchers) {
382         return (matchers[0] = pattern.matcher(arg0)).matches()
383             && (matchers[1] = pattern.matcher(arg1)).matches();
384     }
385 
386     /**
387      * Outputs the list of debugging results returned from a RegexLookup.
388      * Only the matches most similar to the specified xpath will be returned.
389      * @param xpath
390      * @param results
391      */
printLookupResults(String xpath, List<String> results)392     static void printLookupResults(String xpath, List<String> results) {
393         if (results == null) return;
394         System.out.println("Debugging " + xpath + ":");
395         int[] haltPos = new int[results.size()];
396         int furthestPos = 0;
397         for (int i = 0; i < haltPos.length; i++) {
398             int pos = results.get(i).indexOf('\u2639'); // ☹
399             haltPos[i] = pos;
400             if (pos > furthestPos) furthestPos = pos;
401         }
402         for (int i = 0; i < haltPos.length; i++) {
403             if (haltPos[i] < furthestPos) continue;
404             System.out.println(results.get(i));
405         }
406     }
407 
408     RegexLookup<FallbackInfo> fallbackConverter;
409     RegexLookup<RegexResult> xpathConverter;
410 
411     // One FallbackInfo object for every type of rbPath.
412     class FallbackInfo implements Iterable<R3<Finder, String, List<String>>> {
413         // Fallback info in the order: xpath matcher, fallback xpath, fallback values.
414         private List<R3<Finder, String, List<String>>> fallbackItems;
415         private List<Integer> argsUsed; // list of args used by the rb pattern
416         private int numXpathArgs; // Number of args in the xpath
417 
FallbackInfo(List<Integer> argsUsed, int numXpathArgs)418         public FallbackInfo(List<Integer> argsUsed, int numXpathArgs) {
419             fallbackItems = new ArrayList<R3<Finder, String, List<String>>>();
420             this.argsUsed = argsUsed;
421             this.numXpathArgs = numXpathArgs;
422         }
423 
addItem(Finder xpathMatcher, String fallbackXpath, String[] fallbackValues)424         public void addItem(Finder xpathMatcher, String fallbackXpath, String[] fallbackValues) {
425             List<String> values = new ArrayList<String>();
426             for (String fallbackValue : fallbackValues) {
427                 values.add(fallbackValue);
428             }
429             fallbackItems.add(new R3<Finder, String, List<String>>(xpathMatcher, fallbackXpath, values));
430         }
431 
432         /**
433          * Takes in arguments obtained from a RegexLookup on a RB path and fleshes
434          * it out for the corresponding xpath.
435          *
436          * @param arguments
437          * @return
438          */
getArgumentsForXpath(String[] arguments)439         public String[] getArgumentsForXpath(String[] arguments) {
440             String[] output = new String[numXpathArgs + 1];
441             output[0] = arguments[0];
442             for (int i = 0; i < argsUsed.size(); i++) {
443                 output[argsUsed.get(i)] = arguments[i + 1];
444             }
445             for (int i = 0; i < output.length; i++) {
446                 if (output[i] == null) output[i] = "x"; // dummy value
447             }
448             return output;
449         }
450 
getItem(Finder finder)451         private R3<Finder, String, List<String>> getItem(Finder finder) {
452             for (R3<Finder, String, List<String>> item : fallbackItems) {
453                 if (item.get0().equals(finder)) return item;
454             }
455             return null;
456         }
457 
merge(FallbackInfo newInfo)458         public FallbackInfo merge(FallbackInfo newInfo) {
459             for (R3<Finder, String, List<String>> newItem : newInfo.fallbackItems) {
460                 R3<Finder, String, List<String>> item = getItem(newItem.get0());
461                 if (item == null) {
462                     fallbackItems.add(newItem);
463                 } else {
464                     item.get2().addAll(newItem.get2());
465                 }
466             }
467             return this;
468         }
469 
470         @Override
iterator()471         public Iterator<R3<Finder, String, List<String>>> iterator() {
472             return fallbackItems.iterator();
473         }
474     }
475 
476     /**
477      * All child classes should call this constructor.
478      *
479      * @param converterFile
480      */
RegexManager(String converterFile)481     RegexManager(String converterFile) {
482         this.converterFile = converterFile;
483         unprocessedMatchers = new HashMap<String, RegexResult>();
484     }
485 
486     /**
487      * @return a RegexLookup for matching rb paths that may require fallback
488      *         values.
489      */
getFallbackConverter()490     RegexLookup<FallbackInfo> getFallbackConverter() {
491         if (fallbackConverter == null) {
492             loadConverters();
493         }
494         return fallbackConverter;
495     }
496 
497     /**
498      * @return a RegexLookup for matching xpaths
499      */
getPathConverter()500     RegexLookup<RegexResult> getPathConverter() {
501         if (xpathConverter == null) {
502             loadConverters();
503         }
504         return xpathConverter;
505     }
506 
getPathConverter(CLDRFile cldrFile)507     RegexLookup<RegexResult> getPathConverter(CLDRFile cldrFile) {
508         RegexLookup<RegexResult> basePathConverter = getPathConverter();
509 
510         RegexLookup<RegexResult> processedPathConverter = new RegexLookup<RegexResult>()
511             .setPatternTransform(regexTransform);
512         cldrVariables = new VariableReplacer();
513         for (Entry<String, String> entry : xpathVariables.entrySet()) {
514             cldrVariables.add(entry.getKey(), cldrFile.getStringValue(entry.getValue()));
515         }
516         for (Map.Entry<Finder, RegexResult> entry : basePathConverter) {
517             processedPathConverter.add(entry.getKey(), entry.getValue());
518         }
519         for (Entry<String, RegexResult> entry : unprocessedMatchers.entrySet()) {
520             processedPathConverter.add(cldrVariables.replace(entry.getKey()),
521                 entry.getValue());
522         }
523         return processedPathConverter;
524     }
525 
loadConverters()526     private void loadConverters() {
527         xpathConverter = new RegexLookup<RegexResult>()
528             .setPatternTransform(regexTransform);
529         fallbackConverter = new RegexLookup<FallbackInfo>()
530             .setValueMerger(new Merger<FallbackInfo>() {
531                 @Override
532                 public FallbackInfo merge(FallbackInfo a, FallbackInfo into) {
533                     return into.merge(a);
534                 }
535             });
536         xpathVariables = new HashMap<String, String>();
537         BufferedReader reader = FileReaders.openFile(NewLdml2IcuConverter.class, converterFile);
538         VariableReplacer variables = new VariableReplacer();
539         Finder xpathMatcher = null;
540         RegexResult regexResult = null;
541         String line = null;
542         int lineNum = 0;
543         try {
544             while ((line = reader.readLine()) != null) {
545                 lineNum++;
546                 line = line.trim();
547                 // Skip comments.
548                 if (line.length() == 0 || line.startsWith("#")) continue;
549                 // Read variables.
550                 if (line.charAt(0) == '%') {
551                     int pos = line.indexOf("=");
552                     if (pos < 0) {
553                         throw new IllegalArgumentException();
554                     }
555                     String varName = line.substring(0, pos).trim();
556                     String varValue = line.substring(pos + 1).trim();
557                     // Variables representing xpaths should be replaced later on.
558                     if (varValue.startsWith("//")) {
559                         xpathVariables.put(varName, varValue);
560                     } else {
561                         variables.add(varName, varValue);
562                     }
563                     continue;
564                 }
565                 if (line.contains("%")) {
566                     line = variables.replace(line);
567                 }
568                 // if line still contains "%", still unprocessed
569                 // Process a line in the input file for xpath conversion.
570                 String[] content = line.split(SEMI.toString());
571                 // xpath ; rbPath ; value
572                 // Create a reverse lookup for rbPaths to xpaths.
573                 if (!line.startsWith(";")) {
574                     if (regexResult != null) {
575                         if (xpathMatcher.toString().contains("%")) {
576                             unprocessedMatchers.put(xpathMatcher.toString(), regexResult);
577                         } else {
578                             xpathConverter.add(xpathMatcher, regexResult);
579                         }
580                     }
581                     xpathMatcher = new RegexFinder(content[0].replace("[@", "\\[@"));
582                     regexResult = new RegexResult();
583                 }
584                 if (content.length > 1) {
585                     addConverterEntry(xpathMatcher, content, regexResult);
586                 }
587             }
588             xpathConverter.add(xpathMatcher, regexResult);
589         } catch (Exception e) {
590             System.err.println("Error reading " + converterFile + " at line " + lineNum + ": " + line);
591             e.printStackTrace();
592         }
593     }
594 
addConverterEntry(Finder xpathMatcher, String[] content, RegexResult regexResult)595     private void addConverterEntry(Finder xpathMatcher, String[] content,
596         RegexResult regexResult) {
597         String rbPath = content[1];
598         // Find arguments in rbPath.
599         Matcher matcher = ARGUMENT.matcher(rbPath);
600         int splitRbPathArg = -1;
601         while (matcher.find()) {
602             char startChar = rbPath.charAt(matcher.start());
603             char endChar = rbPath.charAt(matcher.end() - 1);
604             boolean shouldSplit = !(startChar == '"' && endChar == '"' ||
605                 startChar == '<' && endChar == '>');
606             if (shouldSplit) {
607                 splitRbPathArg = Integer.parseInt(matcher.group(1));
608                 break;
609             }
610         }
611 
612         // Parse remaining special instructions.
613         Map<String, String> instructions = new HashMap<String, String>();
614         for (int i = 2; i < content.length; i++) {
615             String[] instruction = content[i].split("=", 2);
616             if (instruction[0].equals("fallback")) {
617                 // WARNING: fallback might backfire if more than one type of xpath for the same rbpath
618                 addFallback(xpathMatcher, rbPath, instruction[1]);
619             } else {
620                 instructions.put(instruction[0], instruction[1]);
621             }
622         }
623         regexResult.add(rbPath, instructions, splitRbPathArg);
624     }
625 
626     /**
627      * Adds an entry to the fallback converter.
628      *
629      * @param xpathMatcher
630      *            the xpath matcher that determines if a fallback value
631      *            is necessary
632      * @param rbPath
633      *            the rb path that the fallback value is for
634      * @param fallbackValue
635      *            the fallback value
636      */
addFallback(Finder xpathMatcher, String rbPath, String fallbackValue)637     private void addFallback(Finder xpathMatcher, String rbPath, String fallbackValue) {
638         ArrayList<StringBuffer> args = new ArrayList<StringBuffer>();
639         int numBraces = 0;
640         int argNum = 0;
641         // Create RB path matcher and xpath replacement.
642         // WARNING: doesn't currently take lookaround groups into account.
643         StringBuffer xpathReplacement = new StringBuffer();
644         for (char c : xpathMatcher.toString().toCharArray()) {
645             boolean isBrace = false;
646             if (c == '(') {
647                 numBraces++;
648                 argNum++;
649                 args.add(new StringBuffer());
650                 isBrace = true;
651                 if (numBraces == 1) {
652                     xpathReplacement.append('$').append(argNum);
653                 }
654             }
655             if (numBraces > 0) {
656                 for (int i = args.size() - numBraces; i < args.size(); i++) {
657                     args.get(i).append(c);
658                 }
659             }
660             if (c == ')') {
661                 numBraces--;
662                 isBrace = true;
663             }
664             if (!isBrace && numBraces == 0) {
665                 xpathReplacement.append(c);
666             }
667         }
668         String fallbackXpath = xpathReplacement.toString().replaceAll("\\\\\\[", "[");
669         if (fallbackXpath.contains("(")) {
670             System.err.println("Warning: malformed xpath " + fallbackXpath);
671         }
672 
673         // Create rb matcher.
674         Matcher matcher = ARGUMENT.matcher(rbPath);
675         StringBuffer rbPattern = new StringBuffer();
676         int lastIndex = 0;
677         List<Integer> argsUsed = new ArrayList<Integer>();
678         while (matcher.find()) {
679             rbPattern.append(rbPath.substring(lastIndex, matcher.start()));
680             argNum = Integer.parseInt(matcher.group(1));
681             rbPattern.append(args.get(argNum - 1));
682             argsUsed.add(argNum);
683             lastIndex = matcher.end();
684         }
685         rbPattern.append(rbPath.substring(lastIndex));
686         FallbackInfo info = new FallbackInfo(argsUsed, args.size());
687         info.addItem(xpathMatcher, fallbackXpath, fallbackValue.split("\\s"));
688         fallbackConverter.add(new RegexFinder(rbPattern.toString()), info);
689     }
690 
addFallbackValues(Map<String, CldrArray> pathValueMap)691     void addFallbackValues(Map<String, CldrArray> pathValueMap) {
692         addFallbackValues(null, pathValueMap);
693     }
694 
695     /**
696      * Checks if any fallback values are required and adds them to the specified
697      * map.
698      *
699      * @param pathValueMap
700      */
addFallbackValues(CLDRFile cldrFile, Map<String, CldrArray> pathValueMap)701     void addFallbackValues(CLDRFile cldrFile, Map<String, CldrArray> pathValueMap) {
702         RegexLookup<FallbackInfo> fallbackConverter = getFallbackConverter();
703         for (String rbPath : pathValueMap.keySet()) {
704             Output<String[]> arguments = new Output<String[]>();
705             FallbackInfo fallbackInfo = fallbackConverter.get(rbPath, null, arguments);
706             if (fallbackInfo == null) continue;
707             CldrArray values = pathValueMap.get(rbPath);
708             for (R3<Finder, String, List<String>> info : fallbackInfo) {
709                 if (!values.findKey(info.get0())) {
710                     // The fallback xpath is just for sorting purposes.
711                     String fallbackXpath = processString(info.get1(),
712                         fallbackInfo.getArgumentsForXpath(arguments.value));
713                     // Sanity check.
714                     if (fallbackXpath.contains("$")) {
715                         System.err.println("Warning: " + fallbackXpath + " for " + rbPath +
716                             " still contains unreplaced arguments.");
717                     }
718                     List<String> fallbackValues = info.get2();
719                     List<String> valueList = new ArrayList<String>();
720                     for (String value : fallbackValues) {
721                         value = processString(value, arguments.value);
722                         // Value is an xpath, so get real value from CLDRFile.
723                         if (value.startsWith("//") && cldrFile != null) {
724                             value = cldrFile.getStringValue(cldrVariables.replace(value));
725                         }
726                         valueList.add(value);
727                     }
728                     values.put(fallbackXpath, valueList, null);
729                 }
730             }
731         }
732     }
733 
getCldrArray(String key, Map<String, CldrArray> pathValueMap)734     static CldrArray getCldrArray(String key, Map<String, CldrArray> pathValueMap) {
735         CldrArray cldrArray = pathValueMap.get(key);
736         if (cldrArray == null) {
737             cldrArray = new CldrArray();
738             pathValueMap.put(key, cldrArray);
739         }
740         return cldrArray;
741     }
742 }
743