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