]> gitweb.fperrin.net Git - Dictionary.git/blob - jars/icu4j-4_2_1-src/src/com/ibm/icu/text/TransliteratorIDParser.java
go
[Dictionary.git] / jars / icu4j-4_2_1-src / src / com / ibm / icu / text / TransliteratorIDParser.java
1 /*\r
2 **********************************************************************\r
3 *   Copyright (c) 2002-2009, International Business Machines Corporation\r
4 *   and others.  All Rights Reserved.\r
5 **********************************************************************\r
6 *   Date        Name        Description\r
7 *   01/14/2002  aliu        Creation.\r
8 **********************************************************************\r
9 */\r
10 \r
11 package com.ibm.icu.text;\r
12 \r
13 import com.ibm.icu.util.CaseInsensitiveString;\r
14 import com.ibm.icu.impl.Utility;\r
15 import java.text.ParsePosition;\r
16 import java.util.Hashtable;\r
17 import java.util.Vector;\r
18 \r
19 /**\r
20  * Parsing component for transliterator IDs.  This class contains only\r
21  * static members; it cannot be instantiated.  Methods in this class\r
22  * parse various ID formats, including the following:\r
23  *\r
24  * A basic ID, which contains source, target, and variant, but no\r
25  * filter and no explicit inverse.  Examples include\r
26  * "Latin-Greek/UNGEGN" and "Null".\r
27  *\r
28  * A single ID, which is a basic ID plus optional filter and optional\r
29  * explicit inverse.  Examples include "[a-zA-Z] Latin-Greek" and\r
30  * "Lower (Upper)".\r
31  *\r
32  * A compound ID, which is a sequence of one or more single IDs,\r
33  * separated by semicolons, with optional forward and reverse global\r
34  * filters.  The global filters are UnicodeSet patterns prepended or\r
35  * appended to the IDs, separated by semicolons.  An appended filter\r
36  * must be enclosed in parentheses and applies in the reverse\r
37  * direction.\r
38  *\r
39  * @author Alan Liu\r
40  */\r
41 class TransliteratorIDParser {\r
42 \r
43     private static final char ID_DELIM = ';';\r
44 \r
45     private static final char TARGET_SEP = '-';\r
46 \r
47     private static final char VARIANT_SEP = '/';\r
48 \r
49     private static final char OPEN_REV = '(';\r
50 \r
51     private static final char CLOSE_REV = ')';\r
52 \r
53     private static final String ANY = "Any";\r
54 \r
55     private static final int FORWARD = Transliterator.FORWARD;\r
56 \r
57     private static final int REVERSE = Transliterator.REVERSE;\r
58 \r
59     private static final Hashtable SPECIAL_INVERSES = new Hashtable();\r
60 \r
61     /**\r
62      * A structure containing the parsed data of a filtered ID, that\r
63      * is, a basic ID optionally with a filter.\r
64      *\r
65      * 'source' and 'target' will always be non-null.  The 'variant'\r
66      * will be non-null only if a non-empty variant was parsed.\r
67      *\r
68      * 'sawSource' is true if there was an explicit source in the\r
69      * parsed id.  If there was no explicit source, then an implied\r
70      * source of ANY is returned and 'sawSource' is set to false.\r
71      * \r
72      * 'filter' is the parsed filter pattern, or null if there was no\r
73      * filter.\r
74      */\r
75     private static class Specs {\r
76         public String source; // not null\r
77         public String target; // not null\r
78         public String variant; // may be null\r
79         public String filter; // may be null\r
80         public boolean sawSource;\r
81         Specs(String s, String t, String v, boolean sawS, String f) {\r
82             source = s;\r
83             target = t;\r
84             variant = v;\r
85             sawSource = sawS;\r
86             filter = f;\r
87         }\r
88     }\r
89 \r
90     /**\r
91      * A structure containing the canonicalized data of a filtered ID,\r
92      * that is, a basic ID optionally with a filter.\r
93      *\r
94      * 'canonID' is always non-null.  It may be the empty string "".\r
95      * It is the id that should be assigned to the created\r
96      * transliterator.  It _cannot_ be instantiated directly.\r
97      *\r
98      * 'basicID' is always non-null and non-empty.  It is always of\r
99      * the form S-T or S-T/V.  It is designed to be fed to low-level\r
100      * instantiation code that only understands these two formats.\r
101      *\r
102      * 'filter' may be null, if there is none, or non-null and\r
103      * non-empty.\r
104      */\r
105     static class SingleID {\r
106         public String canonID;\r
107         public String basicID;\r
108         public String filter;\r
109         SingleID(String c, String b, String f) {\r
110             canonID = c;\r
111             basicID = b;\r
112             filter = f;\r
113         }\r
114         SingleID(String c, String b) {\r
115             this(c, b, null);\r
116         }\r
117         Transliterator getInstance() {\r
118             Transliterator t;\r
119             if (basicID == null || basicID.length() == 0) {\r
120                 t = Transliterator.getBasicInstance("Any-Null", canonID);\r
121             } else {\r
122                 t = Transliterator.getBasicInstance(basicID, canonID);\r
123             }\r
124             if (t != null) {\r
125                 if (filter != null) {\r
126                     t.setFilter(new UnicodeSet(filter));\r
127                 }\r
128             }\r
129             return t;\r
130         }\r
131     }\r
132 \r
133     /**\r
134      * Parse a filter ID, that is, an ID of the general form\r
135      * "[f1] s1-t1/v1", with the filters optional, and the variants optional.\r
136      * @param id the id to be parsed\r
137      * @param pos INPUT-OUTPUT parameter.  On input, the position of\r
138      * the first character to parse.  On output, the position after\r
139      * the last character parsed.\r
140      * @return a SingleID object or null if the parse fails\r
141      */\r
142     public static SingleID parseFilterID(String id, int[] pos) {\r
143 \r
144         int start = pos[0];\r
145         Specs specs = parseFilterID(id, pos, true);\r
146         if (specs == null) {\r
147             pos[0] = start;\r
148             return null;\r
149         }\r
150 \r
151         // Assemble return results\r
152         SingleID single = specsToID(specs, FORWARD);\r
153         single.filter = specs.filter;\r
154         return single;\r
155     }\r
156 \r
157     /**\r
158      * Parse a single ID, that is, an ID of the general form\r
159      * "[f1] s1-t1/v1 ([f2] s2-t3/v2)", with the parenthesized element\r
160      * optional, the filters optional, and the variants optional.\r
161      * @param id the id to be parsed\r
162      * @param pos INPUT-OUTPUT parameter.  On input, the position of\r
163      * the first character to parse.  On output, the position after\r
164      * the last character parsed.\r
165      * @param dir the direction.  If the direction is REVERSE then the\r
166      * SingleID is constructed for the reverse direction.\r
167      * @return a SingleID object or null\r
168      */\r
169     public static SingleID parseSingleID(String id, int[] pos, int dir) {\r
170 \r
171         int start = pos[0];\r
172 \r
173         // The ID will be of the form A, A(), A(B), or (B), where\r
174         // A and B are filter IDs.\r
175         Specs specsA = null;\r
176         Specs specsB = null;\r
177         boolean sawParen = false;\r
178 \r
179         // On the first pass, look for (B) or ().  If this fails, then\r
180         // on the second pass, look for A, A(B), or A().\r
181         for (int pass=1; pass<=2; ++pass) {\r
182             if (pass == 2) {\r
183                 specsA = parseFilterID(id, pos, true);\r
184                 if (specsA == null) {\r
185                     pos[0] = start;\r
186                     return null;\r
187                 }\r
188             }\r
189             if (Utility.parseChar(id, pos, OPEN_REV)) {\r
190                 sawParen = true;\r
191                 if (!Utility.parseChar(id, pos, CLOSE_REV)) {\r
192                     specsB = parseFilterID(id, pos, true);\r
193                     // Must close with a ')'\r
194                     if (specsB == null || !Utility.parseChar(id, pos, CLOSE_REV)) {\r
195                         pos[0] = start;\r
196                         return null;\r
197                     }\r
198                 }\r
199                 break;\r
200             }\r
201         }\r
202 \r
203         // Assemble return results\r
204         SingleID single;\r
205         if (sawParen) {\r
206             if (dir == FORWARD) {\r
207                 single = specsToID(specsA, FORWARD);\r
208                 single.canonID = single.canonID +\r
209                     OPEN_REV + specsToID(specsB, FORWARD).canonID + CLOSE_REV;\r
210                 if (specsA != null) {\r
211                     single.filter = specsA.filter;\r
212                 }\r
213             } else {\r
214                 single = specsToID(specsB, FORWARD);\r
215                 single.canonID = single.canonID +\r
216                     OPEN_REV + specsToID(specsA, FORWARD).canonID + CLOSE_REV;\r
217                 if (specsB != null) {\r
218                     single.filter = specsB.filter;\r
219                 }\r
220             }\r
221         } else {\r
222             // assert(specsA != null);\r
223             if (dir == FORWARD) {\r
224                 single = specsToID(specsA, FORWARD);\r
225             } else {\r
226                 single = specsToSpecialInverse(specsA);\r
227                 if (single == null) {\r
228                     single = specsToID(specsA, REVERSE);\r
229                 }\r
230             }\r
231             single.filter = specsA.filter;\r
232         }\r
233 \r
234         return single;\r
235     }\r
236 \r
237     /**\r
238      * Parse a global filter of the form "[f]" or "([f])", depending\r
239      * on 'withParens'.\r
240      * @param id the pattern the parse\r
241      * @param pos INPUT-OUTPUT parameter.  On input, the position of\r
242      * the first character to parse.  On output, the position after\r
243      * the last character parsed.\r
244      * @param dir the direction.\r
245      * @param withParens INPUT-OUTPUT parameter.  On entry, if\r
246      * withParens[0] is 0, then parens are disallowed.  If it is 1,\r
247      * then parens are requires.  If it is -1, then parens are\r
248      * optional, and the return result will be set to 0 or 1.\r
249      * @param canonID OUTPUT parameter.  The pattern for the filter\r
250      * added to the canonID, either at the end, if dir is FORWARD, or\r
251      * at the start, if dir is REVERSE.  The pattern will be enclosed\r
252      * in parentheses if appropriate, and will be suffixed with an\r
253      * ID_DELIM character.  May be null.\r
254      * @return a UnicodeSet object or null.  A non-null results\r
255      * indicates a successful parse, regardless of whether the filter\r
256      * applies to the given direction.  The caller should discard it\r
257      * if withParens != (dir == REVERSE).\r
258      */\r
259     public static UnicodeSet parseGlobalFilter(String id, int[] pos, int dir,\r
260                                                int[] withParens,\r
261                                                StringBuffer canonID) {\r
262         UnicodeSet filter = null;\r
263         int start = pos[0];\r
264 \r
265         if (withParens[0] == -1) {\r
266             withParens[0] = Utility.parseChar(id, pos, OPEN_REV) ? 1 : 0;\r
267         } else if (withParens[0] == 1) {\r
268             if (!Utility.parseChar(id, pos, OPEN_REV)) {\r
269                 pos[0] = start;\r
270                 return null;\r
271             }\r
272         }\r
273         \r
274         Utility.skipWhitespace(id, pos);\r
275 \r
276         if (UnicodeSet.resemblesPattern(id, pos[0])) {\r
277             ParsePosition ppos = new ParsePosition(pos[0]);\r
278             try {\r
279                 filter = new UnicodeSet(id, ppos, null);\r
280             } catch (IllegalArgumentException e) {\r
281                 pos[0] = start;\r
282                 return null;\r
283             }\r
284 \r
285             String pattern = id.substring(pos[0], ppos.getIndex());\r
286             pos[0] = ppos.getIndex();\r
287 \r
288             if (withParens[0] == 1 && !Utility.parseChar(id, pos, CLOSE_REV)) {\r
289                 pos[0] = start;\r
290                 return null;\r
291             }\r
292 \r
293             // In the forward direction, append the pattern to the\r
294             // canonID.  In the reverse, insert it at zero, and invert\r
295             // the presence of parens ("A" <-> "(A)").\r
296             if (canonID != null) {\r
297                 if (dir == FORWARD) {\r
298                     if (withParens[0] == 1) {\r
299                         pattern = String.valueOf(OPEN_REV) + pattern + CLOSE_REV;\r
300                     }\r
301                     canonID.append(pattern + ID_DELIM);\r
302                 } else {\r
303                     if (withParens[0] == 0) {\r
304                         pattern = String.valueOf(OPEN_REV) + pattern + CLOSE_REV;\r
305                     }\r
306                     canonID.insert(0, pattern + ID_DELIM);\r
307                 }\r
308             }\r
309         }\r
310 \r
311         return filter;\r
312     }\r
313 \r
314     /**\r
315      * Parse a compound ID, consisting of an optional forward global\r
316      * filter, a separator, one or more single IDs delimited by\r
317      * separators, an an optional reverse global filter.  The\r
318      * separator is a semicolon.  The global filters are UnicodeSet\r
319      * patterns.  The reverse global filter must be enclosed in\r
320      * parentheses.\r
321      * @param id the pattern the parse\r
322      * @param dir the direction.\r
323      * @param canonID OUTPUT parameter that receives the canonical ID,\r
324      * consisting of canonical IDs for all elements, as returned by\r
325      * parseSingleID(), separated by semicolons.  Previous contents\r
326      * are discarded.\r
327      * @param list OUTPUT parameter that receives a list of SingleID\r
328      * objects representing the parsed IDs.  Previous contents are\r
329      * discarded.\r
330      * @param globalFilter OUTPUT parameter that receives a pointer to\r
331      * a newly created global filter for this ID in this direction, or\r
332      * null if there is none.\r
333      * @return true if the parse succeeds, that is, if the entire\r
334      * id is consumed without syntax error.\r
335      */\r
336     public static boolean parseCompoundID(String id, int dir,\r
337                                           StringBuffer canonID,\r
338                                           Vector list,\r
339                                           UnicodeSet[] globalFilter) {\r
340         int[] pos = new int[] { 0 };\r
341         int[] withParens = new int[1];\r
342         list.removeAllElements();\r
343         UnicodeSet filter;\r
344         globalFilter[0] = null;\r
345         canonID.setLength(0);\r
346 \r
347         // Parse leading global filter, if any\r
348         withParens[0] = 0; // parens disallowed\r
349         filter = parseGlobalFilter(id, pos, dir, withParens, canonID);\r
350         if (filter != null) {\r
351             if (!Utility.parseChar(id, pos, ID_DELIM)) {\r
352                 // Not a global filter; backup and resume\r
353                 canonID.setLength(0);\r
354                 pos[0] = 0;\r
355             }\r
356             if (dir == FORWARD) {\r
357                 globalFilter[0] = filter;\r
358             }\r
359         }\r
360 \r
361         boolean sawDelimiter = true;\r
362         for (;;) {\r
363             SingleID single = parseSingleID(id, pos, dir);\r
364             if (single == null) {\r
365                 break;\r
366             }\r
367             if (dir == FORWARD) {\r
368                 list.addElement(single);\r
369             } else {\r
370                 list.insertElementAt(single, 0);\r
371             }\r
372             if (!Utility.parseChar(id, pos, ID_DELIM)) {\r
373                 sawDelimiter = false;\r
374                 break;\r
375             }\r
376         }\r
377 \r
378         if (list.size() == 0) {\r
379             return false;\r
380         }\r
381 \r
382         // Construct canonical ID\r
383         for (int i=0; i<list.size(); ++i) {\r
384             SingleID single = (SingleID) list.elementAt(i);\r
385             canonID.append(single.canonID);\r
386             if (i != (list.size()-1)) {\r
387                 canonID.append(ID_DELIM);\r
388             }\r
389         }\r
390 \r
391         // Parse trailing global filter, if any, and only if we saw\r
392         // a trailing delimiter after the IDs.\r
393         if (sawDelimiter) {\r
394             withParens[0] = 1; // parens required\r
395             filter = parseGlobalFilter(id, pos, dir, withParens, canonID);\r
396             if (filter != null) {\r
397                 // Don't require trailing ';', but parse it if present\r
398                 Utility.parseChar(id, pos, ID_DELIM);\r
399                 \r
400                 if (dir == REVERSE) {\r
401                     globalFilter[0] = filter;\r
402                 }\r
403             }\r
404         }\r
405 \r
406         // Trailing unparsed text is a syntax error\r
407         Utility.skipWhitespace(id, pos[0]);\r
408         if (pos[0] != id.length()) {\r
409             return false;\r
410         }\r
411 \r
412         return true;\r
413     }\r
414 \r
415     /**\r
416      * Convert the elements of the 'list' vector, which are SingleID\r
417      * objects, into actual Transliterator objects.  In the course of\r
418      * this, some (or all) entries may be removed.  If all entries\r
419      * are removed, the Null transliterator will be added.\r
420      *\r
421      * Delete entries with empty basicIDs; these are generated by\r
422      * elements like "(A)" in the forward direction, or "A()" in\r
423      * the reverse.  THIS MAY RESULT IN AN EMPTY VECTOR.  Convert\r
424      * SingleID entries to actual transliterators.\r
425      *\r
426      * @param list vector of SingleID objects.  On exit, vector\r
427      * of one or more Transliterators.\r
428      */\r
429     public static void instantiateList(Vector list) {\r
430         Transliterator t;\r
431         for (int i=0; i<=list.size(); ) { // [sic]: i<=list.size()\r
432             // We run the loop too long by one, so we can\r
433             // do an insert after the last element\r
434             if (i==list.size()) {\r
435                 break;\r
436             }\r
437             \r
438             SingleID single = (SingleID) list.elementAt(i);\r
439             if (single.basicID.length() == 0) {\r
440                 list.removeElementAt(i);\r
441             } else {\r
442                 t = single.getInstance();\r
443                 if (t == null) {\r
444                     t = single.getInstance(); // looks like this is for debugging...\r
445                     throw new IllegalArgumentException("Illegal ID " + single.canonID);\r
446                 }\r
447                 list.setElementAt(t, i);\r
448                 ++i;\r
449             }\r
450         }\r
451         \r
452         // An empty list is equivalent to a Null transliterator.\r
453         if (list.size() == 0) {\r
454             t = Transliterator.getBasicInstance("Any-Null", null);\r
455             if (t == null) {\r
456                 // Should never happen\r
457                 throw new IllegalArgumentException("Internal error; cannot instantiate Any-Null");\r
458             }\r
459             list.addElement(t);\r
460         }\r
461     }\r
462 \r
463     /**\r
464      * Parse an ID into pieces.  Take IDs of the form T, T/V, S-T,\r
465      * S-T/V, or S/V-T.  If the source is missing, return a source of\r
466      * ANY.\r
467      * @param id the id string, in any of several forms\r
468      * @return an array of 4 strings: source, target, variant, and\r
469      * isSourcePresent.  If the source is not present, ANY will be\r
470      * given as the source, and isSourcePresent will be null.  Otherwise\r
471      * isSourcePresent will be non-null.  The target may be empty if the\r
472      * id is not well-formed.  The variant may be empty.\r
473      */\r
474     public static String[] IDtoSTV(String id) {\r
475         String source = ANY;\r
476         String target = null;\r
477         String variant = "";\r
478         \r
479         int sep = id.indexOf(TARGET_SEP);\r
480         int var = id.indexOf(VARIANT_SEP);\r
481         if (var < 0) {\r
482             var = id.length();\r
483         }\r
484         boolean isSourcePresent = false;\r
485         \r
486         if (sep < 0) {\r
487             // Form: T/V or T (or /V)\r
488             target = id.substring(0, var);\r
489             variant = id.substring(var);\r
490         } else if (sep < var) {\r
491             // Form: S-T/V or S-T (or -T/V or -T)\r
492             if (sep > 0) {\r
493                 source = id.substring(0, sep);\r
494               isSourcePresent = true;\r
495             }\r
496             target = id.substring(++sep, var);\r
497             variant = id.substring(var);\r
498         } else {\r
499             // Form: (S/V-T or /V-T)\r
500             if (var > 0) {\r
501                 source = id.substring(0, var);\r
502                 isSourcePresent = true;\r
503             }\r
504             variant = id.substring(var, sep++);\r
505             target = id.substring(sep);\r
506         }\r
507 \r
508         if (variant.length() > 0) {\r
509             variant = variant.substring(1);\r
510         }\r
511         \r
512         return new String[] { source, target, variant,\r
513                               isSourcePresent ? "" : null };\r
514     }\r
515 \r
516     /**\r
517      * Given source, target, and variant strings, concatenate them into a\r
518      * full ID.  If the source is empty, then "Any" will be used for the\r
519      * source, so the ID will always be of the form s-t/v or s-t.\r
520      */\r
521     public static String STVtoID(String source,\r
522                                  String target,\r
523                                  String variant) {\r
524         StringBuffer id = new StringBuffer(source);\r
525         if (id.length() == 0) {\r
526             id.append(ANY);\r
527         }\r
528         id.append(TARGET_SEP).append(target);\r
529         if (variant != null && variant.length() != 0) {\r
530             id.append(VARIANT_SEP).append(variant);\r
531         }\r
532         return id.toString();\r
533     }\r
534 \r
535     /**\r
536      * Register two targets as being inverses of one another.  For\r
537      * example, calling registerSpecialInverse("NFC", "NFD", true) causes\r
538      * Transliterator to form the following inverse relationships:\r
539      *\r
540      * <pre>NFC => NFD\r
541      * Any-NFC => Any-NFD\r
542      * NFD => NFC\r
543      * Any-NFD => Any-NFC</pre>\r
544      *\r
545      * (Without the special inverse registration, the inverse of NFC\r
546      * would be NFC-Any.)  Note that NFD is shorthand for Any-NFD, but\r
547      * that the presence or absence of "Any-" is preserved.\r
548      *\r
549      * <p>The relationship is symmetrical; registering (a, b) is\r
550      * equivalent to registering (b, a).\r
551      *\r
552      * <p>The relevant IDs must still be registered separately as\r
553      * factories or classes.\r
554      *\r
555      * <p>Only the targets are specified.  Special inverses always\r
556      * have the form Any-Target1 <=> Any-Target2.  The target should\r
557      * have canonical casing (the casing desired to be produced when\r
558      * an inverse is formed) and should contain no whitespace or other\r
559      * extraneous characters.\r
560      *\r
561      * @param target the target against which to register the inverse\r
562      * @param inverseTarget the inverse of target, that is\r
563      * Any-target.getInverse() => Any-inverseTarget\r
564      * @param bidirectional if true, register the reverse relation\r
565      * as well, that is, Any-inverseTarget.getInverse() => Any-target\r
566      */\r
567     public static void registerSpecialInverse(String target,\r
568                                               String inverseTarget,\r
569                                               boolean bidirectional) {\r
570         SPECIAL_INVERSES.put(new CaseInsensitiveString(target), inverseTarget);\r
571         if (bidirectional && !target.equalsIgnoreCase(inverseTarget)) {\r
572             SPECIAL_INVERSES.put(new CaseInsensitiveString(inverseTarget), target);\r
573         }\r
574     }\r
575 \r
576     //----------------------------------------------------------------\r
577     // Private implementation\r
578     //----------------------------------------------------------------\r
579 \r
580     /**\r
581      * Parse an ID into component pieces.  Take IDs of the form T,\r
582      * T/V, S-T, S-T/V, or S/V-T.  If the source is missing, return a\r
583      * source of ANY.\r
584      * @param id the id string, in any of several forms\r
585      * @param pos INPUT-OUTPUT parameter.  On input, pos[0] is the\r
586      * offset of the first character to parse in id.  On output,\r
587      * pos[0] is the offset after the last parsed character.  If the\r
588      * parse failed, pos[0] will be unchanged.\r
589      * @param allowFilter if true, a UnicodeSet pattern is allowed\r
590      * at any location between specs or delimiters, and is returned\r
591      * as the fifth string in the array.\r
592      * @return a Specs object, or null if the parse failed.  If\r
593      * neither source nor target was seen in the parsed id, then the\r
594      * parse fails.  If allowFilter is true, then the parsed filter\r
595      * pattern is returned in the Specs object, otherwise the returned\r
596      * filter reference is null.  If the parse fails for any reason\r
597      * null is returned.\r
598      */\r
599     private static Specs parseFilterID(String id, int[] pos,\r
600                                        boolean allowFilter) {\r
601         String first = null;\r
602         String source = null;\r
603         String target = null;\r
604         String variant = null;\r
605         String filter = null;\r
606         char delimiter = 0;\r
607         int specCount = 0;\r
608         int start = pos[0];\r
609 \r
610         // This loop parses one of the following things with each\r
611         // pass: a filter, a delimiter character (either '-' or '/'),\r
612         // or a spec (source, target, or variant).\r
613         for (;;) {\r
614             Utility.skipWhitespace(id, pos);\r
615             if (pos[0] == id.length()) {\r
616                 break;\r
617             }\r
618 \r
619             // Parse filters\r
620             if (allowFilter && filter == null &&\r
621                 UnicodeSet.resemblesPattern(id, pos[0])) {\r
622 \r
623                 ParsePosition ppos = new ParsePosition(pos[0]);\r
624                 // Parse the set to get the position.\r
625                 new UnicodeSet(id, ppos, null);\r
626                 filter = id.substring(pos[0], ppos.getIndex());\r
627                 pos[0] = ppos.getIndex();\r
628                 continue;\r
629             }\r
630 \r
631             if (delimiter == 0) {\r
632                 char c = id.charAt(pos[0]);\r
633                 if ((c == TARGET_SEP && target == null) ||\r
634                     (c == VARIANT_SEP && variant == null)) {\r
635                     delimiter = c;\r
636                     ++pos[0];\r
637                     continue;\r
638                 }\r
639             }\r
640 \r
641             // We are about to try to parse a spec with no delimiter\r
642             // when we can no longer do so (we can only do so at the\r
643             // start); break.\r
644             if (delimiter == 0 && specCount > 0) {\r
645                 break;\r
646             }\r
647 \r
648             String spec = Utility.parseUnicodeIdentifier(id, pos);\r
649             if (spec == null) {\r
650                 // Note that if there was a trailing delimiter, we\r
651                 // consume it.  So Foo-, Foo/, Foo-Bar/, and Foo/Bar-\r
652                 // are legal.\r
653                 break;\r
654             }\r
655 \r
656             switch (delimiter) {\r
657             case 0:\r
658                 first = spec;\r
659                 break;\r
660             case TARGET_SEP:\r
661                 target = spec;\r
662                 break;\r
663             case VARIANT_SEP:\r
664                 variant = spec;\r
665                 break;\r
666             }\r
667             ++specCount;\r
668             delimiter = 0;\r
669         }\r
670 \r
671         // A spec with no prior character is either source or target,\r
672         // depending on whether an explicit "-target" was seen.\r
673         if (first != null) {\r
674             if (target == null) {\r
675                 target = first;\r
676             } else {\r
677                 source = first;\r
678             }\r
679         }\r
680 \r
681         // Must have either source or target\r
682         if (source == null && target == null) {\r
683             pos[0] = start;\r
684             return null;\r
685         }\r
686 \r
687         // Empty source or target defaults to ANY\r
688         boolean sawSource = true;\r
689         if (source == null) {\r
690             source = ANY;\r
691             sawSource = false;\r
692         }\r
693         if (target == null) {\r
694             target = ANY;\r
695         }\r
696 \r
697         return new Specs(source, target, variant, sawSource, filter);\r
698     }\r
699 \r
700     /**\r
701      * Givens a Spec object, convert it to a SingleID object.  The\r
702      * Spec object is a more unprocessed parse result.  The SingleID\r
703      * object contains information about canonical and basic IDs.\r
704      * @return a SingleID; never returns null.  Returned object always\r
705      * has 'filter' field of null.\r
706      */\r
707     private static SingleID specsToID(Specs specs, int dir) {\r
708         String canonID = "";\r
709         String basicID = "";\r
710         String basicPrefix = "";\r
711         if (specs != null) {\r
712             StringBuffer buf = new StringBuffer();\r
713             if (dir == FORWARD) {\r
714                 if (specs.sawSource) {\r
715                     buf.append(specs.source).append(TARGET_SEP);\r
716                 } else {\r
717                     basicPrefix = specs.source + TARGET_SEP;\r
718                 }\r
719                 buf.append(specs.target);\r
720             } else {\r
721                 buf.append(specs.target).append(TARGET_SEP).append(specs.source);\r
722             }\r
723             if (specs.variant != null) {\r
724                 buf.append(VARIANT_SEP).append(specs.variant);\r
725             }\r
726             basicID = basicPrefix + buf.toString();\r
727             if (specs.filter != null) {\r
728                 buf.insert(0, specs.filter);\r
729             }\r
730             canonID = buf.toString();\r
731         }\r
732         return new SingleID(canonID, basicID);\r
733     }\r
734 \r
735     /**\r
736      * Given a Specs object, return a SingleID representing the\r
737      * special inverse of that ID.  If there is no special inverse\r
738      * then return null.\r
739      * @return a SingleID or null.  Returned object always has\r
740      * 'filter' field of null.\r
741      */\r
742     private static SingleID specsToSpecialInverse(Specs specs) {\r
743         if (!specs.source.equalsIgnoreCase(ANY)) {\r
744             return null;\r
745         }\r
746         String inverseTarget = (String) SPECIAL_INVERSES.get(\r
747             new CaseInsensitiveString(specs.target));\r
748         if (inverseTarget != null) {\r
749             // If the original ID contained "Any-" then make the\r
750             // special inverse "Any-Foo"; otherwise make it "Foo".\r
751             // So "Any-NFC" => "Any-NFD" but "NFC" => "NFD".\r
752             StringBuffer buf = new StringBuffer();\r
753             if (specs.filter != null) {\r
754                 buf.append(specs.filter);\r
755             }\r
756             if (specs.sawSource) {\r
757                 buf.append(ANY).append(TARGET_SEP);\r
758             }\r
759             buf.append(inverseTarget);\r
760 \r
761             String basicID = ANY + TARGET_SEP + inverseTarget;\r
762 \r
763             if (specs.variant != null) {\r
764                 buf.append(VARIANT_SEP).append(specs.variant);\r
765                 basicID = basicID + VARIANT_SEP + specs.variant;\r
766             }\r
767             return new SingleID(buf.toString(), basicID);\r
768         }\r
769         return null;\r
770     }\r
771 }\r
772 \r
773 //eof\r