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