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