001: /*
002: **********************************************************************
003: * Copyright (c) 2002-2006, International Business Machines Corporation
004: * and others. All Rights Reserved.
005: **********************************************************************
006: * Date Name Description
007: * 01/14/2002 aliu Creation.
008: **********************************************************************
009: */
010:
011: package com.ibm.icu.text;
012:
013: import com.ibm.icu.util.CaseInsensitiveString;
014: import com.ibm.icu.impl.Utility;
015: import java.text.ParsePosition;
016: import java.util.Hashtable;
017: import java.util.Vector;
018:
019: /**
020: * Parsing component for transliterator IDs. This class contains only
021: * static members; it cannot be instantiated. Methods in this class
022: * parse various ID formats, including the following:
023: *
024: * A basic ID, which contains source, target, and variant, but no
025: * filter and no explicit inverse. Examples include
026: * "Latin-Greek/UNGEGN" and "Null".
027: *
028: * A single ID, which is a basic ID plus optional filter and optional
029: * explicit inverse. Examples include "[a-zA-Z] Latin-Greek" and
030: * "Lower (Upper)".
031: *
032: * A compound ID, which is a sequence of one or more single IDs,
033: * separated by semicolons, with optional forward and reverse global
034: * filters. The global filters are UnicodeSet patterns prepended or
035: * appended to the IDs, separated by semicolons. An appended filter
036: * must be enclosed in parentheses and applies in the reverse
037: * direction.
038: *
039: * @author Alan Liu
040: */
041: class TransliteratorIDParser {
042:
043: private static final char ID_DELIM = ';';
044:
045: private static final char TARGET_SEP = '-';
046:
047: private static final char VARIANT_SEP = '/';
048:
049: private static final char OPEN_REV = '(';
050:
051: private static final char CLOSE_REV = ')';
052:
053: private static final String ANY = "Any";
054:
055: private static final int FORWARD = Transliterator.FORWARD;
056:
057: private static final int REVERSE = Transliterator.REVERSE;
058:
059: private static final Hashtable SPECIAL_INVERSES = new Hashtable();
060:
061: /**
062: * A structure containing the parsed data of a filtered ID, that
063: * is, a basic ID optionally with a filter.
064: *
065: * 'source' and 'target' will always be non-null. The 'variant'
066: * will be non-null only if a non-empty variant was parsed.
067: *
068: * 'sawSource' is true if there was an explicit source in the
069: * parsed id. If there was no explicit source, then an implied
070: * source of ANY is returned and 'sawSource' is set to false.
071: *
072: * 'filter' is the parsed filter pattern, or null if there was no
073: * filter.
074: */
075: private static class Specs {
076: public String source; // not null
077: public String target; // not null
078: public String variant; // may be null
079: public String filter; // may be null
080: public boolean sawSource;
081:
082: Specs(String s, String t, String v, boolean sawS, String f) {
083: source = s;
084: target = t;
085: variant = v;
086: sawSource = sawS;
087: filter = f;
088: }
089: }
090:
091: /**
092: * A structure containing the canonicalized data of a filtered ID,
093: * that is, a basic ID optionally with a filter.
094: *
095: * 'canonID' is always non-null. It may be the empty string "".
096: * It is the id that should be assigned to the created
097: * transliterator. It _cannot_ be instantiated directly.
098: *
099: * 'basicID' is always non-null and non-empty. It is always of
100: * the form S-T or S-T/V. It is designed to be fed to low-level
101: * instantiation code that only understands these two formats.
102: *
103: * 'filter' may be null, if there is none, or non-null and
104: * non-empty.
105: */
106: static class SingleID {
107: public String canonID;
108: public String basicID;
109: public String filter;
110:
111: SingleID(String c, String b, String f) {
112: canonID = c;
113: basicID = b;
114: filter = f;
115: }
116:
117: SingleID(String c, String b) {
118: this (c, b, null);
119: }
120:
121: Transliterator getInstance() {
122: Transliterator t;
123: if (basicID == null || basicID.length() == 0) {
124: t = Transliterator
125: .getBasicInstance("Any-Null", canonID);
126: } else {
127: t = Transliterator.getBasicInstance(basicID, canonID);
128: }
129: if (t != null) {
130: if (filter != null) {
131: t.setFilter(new UnicodeSet(filter));
132: }
133: }
134: return t;
135: }
136: }
137:
138: /**
139: * Parse a filter ID, that is, an ID of the general form
140: * "[f1] s1-t1/v1", with the filters optional, and the variants optional.
141: * @param id the id to be parsed
142: * @param pos INPUT-OUTPUT parameter. On input, the position of
143: * the first character to parse. On output, the position after
144: * the last character parsed.
145: * @return a SingleID object or null if the parse fails
146: */
147: public static SingleID parseFilterID(String id, int[] pos) {
148:
149: int start = pos[0];
150: Specs specs = parseFilterID(id, pos, true);
151: if (specs == null) {
152: pos[0] = start;
153: return null;
154: }
155:
156: // Assemble return results
157: SingleID single = specsToID(specs, FORWARD);
158: single.filter = specs.filter;
159: return single;
160: }
161:
162: /**
163: * Parse a single ID, that is, an ID of the general form
164: * "[f1] s1-t1/v1 ([f2] s2-t3/v2)", with the parenthesized element
165: * optional, the filters optional, and the variants optional.
166: * @param id the id to be parsed
167: * @param pos INPUT-OUTPUT parameter. On input, the position of
168: * the first character to parse. On output, the position after
169: * the last character parsed.
170: * @param dir the direction. If the direction is REVERSE then the
171: * SingleID is constructed for the reverse direction.
172: * @return a SingleID object or null
173: */
174: public static SingleID parseSingleID(String id, int[] pos, int dir) {
175:
176: int start = pos[0];
177:
178: // The ID will be of the form A, A(), A(B), or (B), where
179: // A and B are filter IDs.
180: Specs specsA = null;
181: Specs specsB = null;
182: boolean sawParen = false;
183:
184: // On the first pass, look for (B) or (). If this fails, then
185: // on the second pass, look for A, A(B), or A().
186: for (int pass = 1; pass <= 2; ++pass) {
187: if (pass == 2) {
188: specsA = parseFilterID(id, pos, true);
189: if (specsA == null) {
190: pos[0] = start;
191: return null;
192: }
193: }
194: if (Utility.parseChar(id, pos, OPEN_REV)) {
195: sawParen = true;
196: if (!Utility.parseChar(id, pos, CLOSE_REV)) {
197: specsB = parseFilterID(id, pos, true);
198: // Must close with a ')'
199: if (specsB == null
200: || !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 + OPEN_REV
215: + specsToID(specsB, FORWARD).canonID
216: + CLOSE_REV;
217: if (specsA != null) {
218: single.filter = specsA.filter;
219: }
220: } else {
221: single = specsToID(specsB, FORWARD);
222: single.canonID = single.canonID + OPEN_REV
223: + specsToID(specsA, FORWARD).canonID
224: + CLOSE_REV;
225: if (specsB != null) {
226: single.filter = specsB.filter;
227: }
228: }
229: } else {
230: // assert(specsA != null);
231: if (dir == FORWARD) {
232: single = specsToID(specsA, FORWARD);
233: } else {
234: single = specsToSpecialInverse(specsA);
235: if (single == null) {
236: single = specsToID(specsA, REVERSE);
237: }
238: }
239: single.filter = specsA.filter;
240: }
241:
242: return single;
243: }
244:
245: /**
246: * Parse a global filter of the form "[f]" or "([f])", depending
247: * on 'withParens'.
248: * @param id the pattern the parse
249: * @param pos INPUT-OUTPUT parameter. On input, the position of
250: * the first character to parse. On output, the position after
251: * the last character parsed.
252: * @param dir the direction.
253: * @param withParens INPUT-OUTPUT parameter. On entry, if
254: * withParens[0] is 0, then parens are disallowed. If it is 1,
255: * then parens are requires. If it is -1, then parens are
256: * optional, and the return result will be set to 0 or 1.
257: * @param canonID OUTPUT parameter. The pattern for the filter
258: * added to the canonID, either at the end, if dir is FORWARD, or
259: * at the start, if dir is REVERSE. The pattern will be enclosed
260: * in parentheses if appropriate, and will be suffixed with an
261: * ID_DELIM character. May be null.
262: * @return a UnicodeSet object or null. A non-null results
263: * indicates a successful parse, regardless of whether the filter
264: * applies to the given direction. The caller should discard it
265: * if withParens != (dir == REVERSE).
266: */
267: public static UnicodeSet parseGlobalFilter(String id, int[] pos,
268: int dir, int[] withParens, StringBuffer canonID) {
269: UnicodeSet filter = null;
270: int start = pos[0];
271:
272: if (withParens[0] == -1) {
273: withParens[0] = Utility.parseChar(id, pos, OPEN_REV) ? 1
274: : 0;
275: } else if (withParens[0] == 1) {
276: if (!Utility.parseChar(id, pos, OPEN_REV)) {
277: pos[0] = start;
278: return null;
279: }
280: }
281:
282: Utility.skipWhitespace(id, pos);
283:
284: if (UnicodeSet.resemblesPattern(id, pos[0])) {
285: ParsePosition ppos = new ParsePosition(pos[0]);
286: try {
287: filter = new UnicodeSet(id, ppos, null);
288: } catch (IllegalArgumentException e) {
289: pos[0] = start;
290: return null;
291: }
292:
293: String pattern = id.substring(pos[0], ppos.getIndex());
294: pos[0] = ppos.getIndex();
295:
296: if (withParens[0] == 1
297: && !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
309: + CLOSE_REV;
310: }
311: canonID.append(pattern + ID_DELIM);
312: } else {
313: if (withParens[0] == 0) {
314: pattern = String.valueOf(OPEN_REV) + pattern
315: + CLOSE_REV;
316: }
317: canonID.insert(0, pattern + ID_DELIM);
318: }
319: }
320: }
321:
322: return filter;
323: }
324:
325: /**
326: * Parse a compound ID, consisting of an optional forward global
327: * filter, a separator, one or more single IDs delimited by
328: * separators, an an optional reverse global filter. The
329: * separator is a semicolon. The global filters are UnicodeSet
330: * patterns. The reverse global filter must be enclosed in
331: * parentheses.
332: * @param id the pattern the parse
333: * @param dir the direction.
334: * @param canonID OUTPUT parameter that receives the canonical ID,
335: * consisting of canonical IDs for all elements, as returned by
336: * parseSingleID(), separated by semicolons. Previous contents
337: * are discarded.
338: * @param list OUTPUT parameter that receives a list of SingleID
339: * objects representing the parsed IDs. Previous contents are
340: * discarded.
341: * @param globalFilter OUTPUT parameter that receives a pointer to
342: * a newly created global filter for this ID in this direction, or
343: * null if there is none.
344: * @return true if the parse succeeds, that is, if the entire
345: * id is consumed without syntax error.
346: */
347: public static boolean parseCompoundID(String id, int dir,
348: StringBuffer canonID, Vector list, UnicodeSet[] globalFilter) {
349: int[] pos = new int[] { 0 };
350: int[] withParens = new int[1];
351: list.removeAllElements();
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.addElement(single);
378: } else {
379: list.insertElementAt(single, 0);
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 = (SingleID) list.elementAt(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,
405: canonID);
406: if (filter != null) {
407: // Don't require trailing ';', but parse it if present
408: Utility.parseChar(id, pos, ID_DELIM);
409:
410: if (dir == REVERSE) {
411: globalFilter[0] = filter;
412: }
413: }
414: }
415:
416: // Trailing unparsed text is a syntax error
417: Utility.skipWhitespace(id, pos[0]);
418: if (pos[0] != id.length()) {
419: return false;
420: }
421:
422: return true;
423: }
424:
425: /**
426: * Convert the elements of the 'list' vector, which are SingleID
427: * objects, into actual Transliterator objects. In the course of
428: * this, some (or all) entries may be removed. If all entries
429: * are removed, the Null transliterator will be added.
430: *
431: * Delete entries with empty basicIDs; these are generated by
432: * elements like "(A)" in the forward direction, or "A()" in
433: * the reverse. THIS MAY RESULT IN AN EMPTY VECTOR. Convert
434: * SingleID entries to actual transliterators.
435: *
436: * @param list vector of SingleID objects. On exit, vector
437: * of one or more Transliterators.
438: */
439: public static void instantiateList(Vector list) {
440: Transliterator t;
441: for (int i = 0; i <= list.size();) { // [sic]: i<=list.size()
442: // We run the loop too long by one, so we can
443: // do an insert after the last element
444: if (i == list.size()) {
445: break;
446: }
447:
448: SingleID single = (SingleID) list.elementAt(i);
449: if (single.basicID.length() == 0) {
450: list.removeElementAt(i);
451: } else {
452: t = single.getInstance();
453: if (t == null) {
454: t = single.getInstance();
455: throw new IllegalArgumentException("Illegal ID "
456: + single.canonID);
457: }
458: list.setElementAt(t, i);
459: ++i;
460: }
461: }
462:
463: // An empty list is equivalent to a Null transliterator.
464: if (list.size() == 0) {
465: t = Transliterator.getBasicInstance("Any-Null", null);
466: if (t == null) {
467: // Should never happen
468: throw new IllegalArgumentException(
469: "Internal error; cannot instantiate Any-Null");
470: }
471: list.addElement(t);
472: }
473: }
474:
475: /**
476: * Parse an ID into pieces. Take IDs of the form T, T/V, S-T,
477: * S-T/V, or S/V-T. If the source is missing, return a source of
478: * ANY.
479: * @param id the id string, in any of several forms
480: * @return an array of 4 strings: source, target, variant, and
481: * isSourcePresent. If the source is not present, ANY will be
482: * given as the source, and isSourcePresent will be null. Otherwise
483: * isSourcePresent will be non-null. The target may be empty if the
484: * id is not well-formed. The variant may be empty.
485: */
486: public static String[] IDtoSTV(String id) {
487: String source = ANY;
488: String target = null;
489: String variant = "";
490:
491: int sep = id.indexOf(TARGET_SEP);
492: int var = id.indexOf(VARIANT_SEP);
493: if (var < 0) {
494: var = id.length();
495: }
496: boolean isSourcePresent = false;
497:
498: if (sep < 0) {
499: // Form: T/V or T (or /V)
500: target = id.substring(0, var);
501: variant = id.substring(var);
502: } else if (sep < var) {
503: // Form: S-T/V or S-T (or -T/V or -T)
504: if (sep > 0) {
505: source = id.substring(0, sep);
506: isSourcePresent = true;
507: }
508: target = id.substring(++sep, var);
509: variant = id.substring(var);
510: } else {
511: // Form: (S/V-T or /V-T)
512: if (var > 0) {
513: source = id.substring(0, var);
514: isSourcePresent = true;
515: }
516: variant = id.substring(var, sep++);
517: target = id.substring(sep);
518: }
519:
520: if (variant.length() > 0) {
521: variant = variant.substring(1);
522: }
523:
524: return new String[] { source, target, variant,
525: isSourcePresent ? "" : null };
526: }
527:
528: /**
529: * Given source, target, and variant strings, concatenate them into a
530: * full ID. If the source is empty, then "Any" will be used for the
531: * source, so the ID will always be of the form s-t/v or s-t.
532: */
533: public static String STVtoID(String source, String target,
534: String variant) {
535: StringBuffer id = new StringBuffer(source);
536: if (id.length() == 0) {
537: id.append(ANY);
538: }
539: id.append(TARGET_SEP).append(target);
540: if (variant != null && variant.length() != 0) {
541: id.append(VARIANT_SEP).append(variant);
542: }
543: return id.toString();
544: }
545:
546: /**
547: * Register two targets as being inverses of one another. For
548: * example, calling registerSpecialInverse("NFC", "NFD", true) causes
549: * Transliterator to form the following inverse relationships:
550: *
551: * <pre>NFC => NFD
552: * Any-NFC => Any-NFD
553: * NFD => NFC
554: * Any-NFD => Any-NFC</pre>
555: *
556: * (Without the special inverse registration, the inverse of NFC
557: * would be NFC-Any.) Note that NFD is shorthand for Any-NFD, but
558: * that the presence or absence of "Any-" is preserved.
559: *
560: * <p>The relationship is symmetrical; registering (a, b) is
561: * equivalent to registering (b, a).
562: *
563: * <p>The relevant IDs must still be registered separately as
564: * factories or classes.
565: *
566: * <p>Only the targets are specified. Special inverses always
567: * have the form Any-Target1 <=> Any-Target2. The target should
568: * have canonical casing (the casing desired to be produced when
569: * an inverse is formed) and should contain no whitespace or other
570: * extraneous characters.
571: *
572: * @param target the target against which to register the inverse
573: * @param inverseTarget the inverse of target, that is
574: * Any-target.getInverse() => Any-inverseTarget
575: * @param bidirectional if true, register the reverse relation
576: * as well, that is, Any-inverseTarget.getInverse() => Any-target
577: */
578: public static void registerSpecialInverse(String target,
579: String inverseTarget, boolean bidirectional) {
580: SPECIAL_INVERSES.put(new CaseInsensitiveString(target),
581: inverseTarget);
582: if (bidirectional && !target.equalsIgnoreCase(inverseTarget)) {
583: SPECIAL_INVERSES.put(new CaseInsensitiveString(
584: inverseTarget), target);
585: }
586: }
587:
588: //----------------------------------------------------------------
589: // Private implementation
590: //----------------------------------------------------------------
591:
592: /**
593: * Parse an ID into component pieces. Take IDs of the form T,
594: * T/V, S-T, S-T/V, or S/V-T. If the source is missing, return a
595: * source of ANY.
596: * @param id the id string, in any of several forms
597: * @param pos INPUT-OUTPUT parameter. On input, pos[0] is the
598: * offset of the first character to parse in id. On output,
599: * pos[0] is the offset after the last parsed character. If the
600: * parse failed, pos[0] will be unchanged.
601: * @param allowFilter if true, a UnicodeSet pattern is allowed
602: * at any location between specs or delimiters, and is returned
603: * as the fifth string in the array.
604: * @return a Specs object, or null if the parse failed. If
605: * neither source nor target was seen in the parsed id, then the
606: * parse fails. If allowFilter is true, then the parsed filter
607: * pattern is returned in the Specs object, otherwise the returned
608: * filter reference is null. If the parse fails for any reason
609: * null is returned.
610: */
611: private static Specs parseFilterID(String id, int[] pos,
612: boolean allowFilter) {
613: String first = null;
614: String source = null;
615: String target = null;
616: String variant = null;
617: String filter = null;
618: char delimiter = 0;
619: int specCount = 0;
620: int start = pos[0];
621:
622: // This loop parses one of the following things with each
623: // pass: a filter, a delimiter character (either '-' or '/'),
624: // or a spec (source, target, or variant).
625: for (;;) {
626: Utility.skipWhitespace(id, pos);
627: if (pos[0] == id.length()) {
628: break;
629: }
630:
631: // Parse filters
632: if (allowFilter && filter == null
633: && UnicodeSet.resemblesPattern(id, pos[0])) {
634:
635: ParsePosition ppos = new ParsePosition(pos[0]);
636: UnicodeSet set = new UnicodeSet(id, ppos, null);
637: filter = id.substring(pos[0], ppos.getIndex());
638: pos[0] = ppos.getIndex();
639: continue;
640: }
641:
642: if (delimiter == 0) {
643: char c = id.charAt(pos[0]);
644: if ((c == TARGET_SEP && target == null)
645: || (c == VARIANT_SEP && variant == null)) {
646: delimiter = c;
647: ++pos[0];
648: continue;
649: }
650: }
651:
652: // We are about to try to parse a spec with no delimiter
653: // when we can no longer do so (we can only do so at the
654: // start); break.
655: if (delimiter == 0 && specCount > 0) {
656: break;
657: }
658:
659: String spec = Utility.parseUnicodeIdentifier(id, pos);
660: if (spec == null) {
661: // Note that if there was a trailing delimiter, we
662: // consume it. So Foo-, Foo/, Foo-Bar/, and Foo/Bar-
663: // are legal.
664: break;
665: }
666:
667: switch (delimiter) {
668: case 0:
669: first = spec;
670: break;
671: case TARGET_SEP:
672: target = spec;
673: break;
674: case VARIANT_SEP:
675: variant = spec;
676: break;
677: }
678: ++specCount;
679: delimiter = 0;
680: }
681:
682: // A spec with no prior character is either source or target,
683: // depending on whether an explicit "-target" was seen.
684: if (first != null) {
685: if (target == null) {
686: target = first;
687: } else {
688: source = first;
689: }
690: }
691:
692: // Must have either source or target
693: if (source == null && target == null) {
694: pos[0] = start;
695: return null;
696: }
697:
698: // Empty source or target defaults to ANY
699: boolean sawSource = true;
700: if (source == null) {
701: source = ANY;
702: sawSource = false;
703: }
704: if (target == null) {
705: target = ANY;
706: }
707:
708: return new Specs(source, target, variant, sawSource, filter);
709: }
710:
711: /**
712: * Givens a Spec object, convert it to a SingleID object. The
713: * Spec object is a more unprocessed parse result. The SingleID
714: * object contains information about canonical and basic IDs.
715: * @return a SingleID; never returns null. Returned object always
716: * has 'filter' field of null.
717: */
718: private static SingleID specsToID(Specs specs, int dir) {
719: String canonID = "";
720: String basicID = "";
721: String basicPrefix = "";
722: if (specs != null) {
723: StringBuffer buf = new StringBuffer();
724: if (dir == FORWARD) {
725: if (specs.sawSource) {
726: buf.append(specs.source).append(TARGET_SEP);
727: } else {
728: basicPrefix = specs.source + TARGET_SEP;
729: }
730: buf.append(specs.target);
731: } else {
732: buf.append(specs.target).append(TARGET_SEP).append(
733: specs.source);
734: }
735: if (specs.variant != null) {
736: buf.append(VARIANT_SEP).append(specs.variant);
737: }
738: basicID = basicPrefix + buf.toString();
739: if (specs.filter != null) {
740: buf.insert(0, specs.filter);
741: }
742: canonID = buf.toString();
743: }
744: return new SingleID(canonID, basicID);
745: }
746:
747: /**
748: * Given a Specs object, return a SingleID representing the
749: * special inverse of that ID. If there is no special inverse
750: * then return null.
751: * @return a SingleID or null. Returned object always has
752: * 'filter' field of null.
753: */
754: private static SingleID specsToSpecialInverse(Specs specs) {
755: if (!specs.source.equalsIgnoreCase(ANY)) {
756: return null;
757: }
758: String inverseTarget = (String) SPECIAL_INVERSES
759: .get(new CaseInsensitiveString(specs.target));
760: if (inverseTarget != null) {
761: // If the original ID contained "Any-" then make the
762: // special inverse "Any-Foo"; otherwise make it "Foo".
763: // So "Any-NFC" => "Any-NFD" but "NFC" => "NFD".
764: StringBuffer buf = new StringBuffer();
765: if (specs.filter != null) {
766: buf.append(specs.filter);
767: }
768: if (specs.sawSource) {
769: buf.append(ANY).append(TARGET_SEP);
770: }
771: buf.append(inverseTarget);
772:
773: String basicID = ANY + TARGET_SEP + inverseTarget;
774:
775: if (specs.variant != null) {
776: buf.append(VARIANT_SEP).append(specs.variant);
777: basicID = basicID + VARIANT_SEP + specs.variant;
778: }
779: return new SingleID(buf.toString(), basicID);
780: }
781: return null;
782: }
783: }
784:
785: //eof
|