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