1 /* 2 ****************************************************************************** 3 * Copyright (C) 2004-2013, International Business Machines Corporation and * 4 * others. All Rights Reserved. * 5 ****************************************************************************** 6 */ 7 package org.unicode.cldr.util; 8 9 import java.io.PrintWriter; 10 import java.util.ArrayList; 11 import java.util.Collection; 12 import java.util.Collections; 13 import java.util.EnumMap; 14 import java.util.HashMap; 15 import java.util.Iterator; 16 import java.util.List; 17 import java.util.Map; 18 import java.util.Map.Entry; 19 import java.util.Set; 20 import java.util.TreeMap; 21 import java.util.concurrent.ConcurrentHashMap; 22 23 import com.google.common.collect.ImmutableSet; 24 import com.google.common.collect.ImmutableSet.Builder; 25 import com.ibm.icu.impl.Utility; 26 import com.ibm.icu.util.Freezable; 27 28 /** 29 * Parser for XPath 30 * 31 * Each XPathParts object describes a single path, with its xPath member, for example 32 * //ldml/characters/exemplarCharacters[@type="auxiliary"] 33 * and a list of Element objects that depend on xPath. 34 * Each Element object has an "element" string such as "ldml", "characters", or "exemplarCharacters", 35 * plus attributes such as a Map from key "type" to value "auxiliary". 36 */ 37 public final class XPathParts implements Freezable<XPathParts> { 38 private static final boolean DEBUGGING = false; 39 40 private volatile boolean frozen = false; 41 42 private List<Element> elements = new ArrayList<Element>(); 43 44 private DtdData dtdData = null; 45 46 private Map<String, Map<String, String>> suppressionMap; 47 48 private static final Map<String, XPathParts> cache = new ConcurrentHashMap<String, XPathParts>(); 49 XPathParts()50 public XPathParts() { 51 this.suppressionMap = null; 52 } 53 XPathParts(Map<String, Map<String, String>> suppressionMap)54 public XPathParts(Map<String, Map<String, String>> suppressionMap) { 55 this.suppressionMap = suppressionMap; 56 } 57 58 /** 59 * See if the xpath contains an element 60 */ containsElement(String element)61 public boolean containsElement(String element) { 62 for (int i = 0; i < elements.size(); ++i) { 63 if (elements.get(i).getElement().equals(element)) { 64 return true; 65 } 66 } 67 return false; 68 } 69 70 /** 71 * Empty the xpath 72 * 73 * Called by JsonConverter.rewrite() and CLDRFile.write() 74 */ clear()75 public XPathParts clear() { 76 elements.clear(); 77 dtdData = null; 78 return this; 79 } 80 81 /** 82 * Write out the difference from this xpath and the last, putting the value in the right place. Closes up the 83 * elements that were not closed, and opens up the new. 84 * 85 * @param pw 86 * @param filteredXPath 87 * @param lastFullXPath 88 * @param v 89 * @param xpath_comments 90 * @return this XPathParts 91 */ writeDifference(PrintWriter pw, XPathParts filteredXPath, XPathParts lastFullXPath, String v, Comments xpath_comments)92 public XPathParts writeDifference(PrintWriter pw, XPathParts filteredXPath, XPathParts lastFullXPath, 93 String v, Comments xpath_comments) { 94 int limit = findFirstDifference(lastFullXPath); 95 // write the end of the last one 96 for (int i = lastFullXPath.size() - 2; i >= limit; --i) { 97 pw.print(Utility.repeat("\t", i)); 98 pw.println(lastFullXPath.elements.get(i).toString(XML_CLOSE)); 99 } 100 if (v == null) { 101 return this; // end 102 } 103 // now write the start of the current 104 for (int i = limit; i < size() - 1; ++i) { 105 if (xpath_comments != null) { 106 filteredXPath.writeComment(pw, xpath_comments, i + 1, Comments.CommentType.PREBLOCK); 107 } 108 pw.print(Utility.repeat("\t", i)); 109 pw.println(elements.get(i).toString(XML_OPEN)); 110 } 111 if (xpath_comments != null) { 112 filteredXPath.writeComment(pw, xpath_comments, size(), Comments.CommentType.PREBLOCK); 113 } 114 115 // now write element itself 116 pw.print(Utility.repeat("\t", (size() - 1))); 117 Element e = elements.get(size() - 1); 118 String eValue = v; 119 if (eValue.length() == 0) { 120 pw.print(e.toString(XML_NO_VALUE)); 121 } else { 122 pw.print(e.toString(XML_OPEN)); 123 pw.print(untrim(eValue, size())); 124 pw.print(e.toString(XML_CLOSE)); 125 } 126 if (xpath_comments != null) { 127 filteredXPath.writeComment(pw, xpath_comments, size(), Comments.CommentType.LINE); 128 } 129 pw.println(); 130 if (xpath_comments != null) { 131 filteredXPath.writeComment(pw, xpath_comments, size(), Comments.CommentType.POSTBLOCK); 132 } 133 pw.flush(); 134 return this; 135 } 136 untrim(String eValue, int count)137 private String untrim(String eValue, int count) { 138 String result = TransliteratorUtilities.toHTML.transliterate(eValue); 139 if (!result.contains("\n")) { 140 return result; 141 } 142 String spacer = "\n" + Utility.repeat("\t", count); 143 result = result.replace("\n", spacer); 144 return result; 145 } 146 147 public static class Comments implements Cloneable { 148 public enum CommentType { 149 LINE, PREBLOCK, POSTBLOCK 150 } 151 152 private EnumMap<CommentType, Map<String, String>> comments = new EnumMap<CommentType, Map<String, String>>( 153 CommentType.class); 154 Comments()155 public Comments() { 156 for (CommentType c : CommentType.values()) { 157 comments.put(c, new HashMap<String, String>()); 158 } 159 } 160 getComment(CommentType style, String xpath)161 public String getComment(CommentType style, String xpath) { 162 return comments.get(style).get(xpath); 163 } 164 addComment(CommentType style, String xpath, String comment)165 public Comments addComment(CommentType style, String xpath, String comment) { 166 String existing = comments.get(style).get(xpath); 167 if (existing != null) { 168 comment = existing + XPathParts.NEWLINE + comment; 169 } 170 comments.get(style).put(xpath, comment); 171 return this; 172 } 173 removeComment(CommentType style, String xPath)174 public String removeComment(CommentType style, String xPath) { 175 String result = comments.get(style).get(xPath); 176 if (result != null) comments.get(style).remove(xPath); 177 return result; 178 } 179 extractCommentsWithoutBase()180 public List<String> extractCommentsWithoutBase() { 181 List<String> result = new ArrayList<String>(); 182 for (CommentType style : CommentType.values()) { 183 for (Iterator<String> it = comments.get(style).keySet().iterator(); it.hasNext();) { 184 String key = it.next(); 185 String value = comments.get(style).get(key); 186 result.add(value + "\t - was on: " + key); 187 it.remove(); 188 } 189 } 190 return result; 191 } 192 clone()193 public Object clone() { 194 try { 195 Comments result = (Comments) super.clone(); 196 for (CommentType c : CommentType.values()) { 197 result.comments.put(c, new HashMap<String, String>(comments.get(c))); 198 } 199 return result; 200 } catch (CloneNotSupportedException e) { 201 throw new InternalError("should never happen"); 202 } 203 } 204 205 /** 206 * @param other 207 */ joinAll(Comments other)208 public Comments joinAll(Comments other) { 209 for (CommentType c : CommentType.values()) { 210 CldrUtility.joinWithSeparation(comments.get(c), XPathParts.NEWLINE, other.comments.get(c)); 211 } 212 return this; 213 } 214 215 /** 216 * @param string 217 */ removeComment(String string)218 public Comments removeComment(String string) { 219 if (initialComment.equals(string)) initialComment = ""; 220 if (finalComment.equals(string)) finalComment = ""; 221 for (CommentType c : CommentType.values()) { 222 for (Iterator<String> it = comments.get(c).keySet().iterator(); it.hasNext();) { 223 String key = it.next(); 224 String value = comments.get(c).get(key); 225 if (!value.equals(string)) continue; 226 it.remove(); 227 } 228 } 229 return this; 230 } 231 232 private String initialComment = ""; 233 private String finalComment = ""; 234 235 /** 236 * @return Returns the finalComment. 237 */ getFinalComment()238 public String getFinalComment() { 239 return finalComment; 240 } 241 242 /** 243 * @param finalComment 244 * The finalComment to set. 245 */ setFinalComment(String finalComment)246 public Comments setFinalComment(String finalComment) { 247 this.finalComment = finalComment; 248 return this; 249 } 250 251 /** 252 * @return Returns the initialComment. 253 */ getInitialComment()254 public String getInitialComment() { 255 return initialComment; 256 } 257 258 /** 259 * @param initialComment 260 * The initialComment to set. 261 */ setInitialComment(String initialComment)262 public Comments setInitialComment(String initialComment) { 263 this.initialComment = initialComment; 264 return this; 265 } 266 } 267 268 /** 269 * @param pw 270 * @param xpath_comments 271 * @param index 272 * TODO 273 */ writeComment(PrintWriter pw, Comments xpath_comments, int index, Comments.CommentType style)274 private XPathParts writeComment(PrintWriter pw, Comments xpath_comments, int index, Comments.CommentType style) { 275 if (index == 0) return this; 276 String xpath = toString(index); 277 Log.logln(DEBUGGING, "Checking for: " + xpath); 278 String comment = xpath_comments.removeComment(style, xpath); 279 if (comment != null) { 280 boolean blockComment = style != Comments.CommentType.LINE; 281 XPathParts.writeComment(pw, index - 1, comment, blockComment); 282 } 283 return this; 284 } 285 286 /** 287 * Finds the first place where the xpaths differ. 288 */ findFirstDifference(XPathParts last)289 public int findFirstDifference(XPathParts last) { 290 int min = elements.size(); 291 if (last.elements.size() < min) min = last.elements.size(); 292 for (int i = 0; i < min; ++i) { 293 Element e1 = elements.get(i); 294 Element e2 = last.elements.get(i); 295 if (!e1.equals(e2)) return i; 296 } 297 return min; 298 } 299 300 /** 301 * Checks if the new xpath given is like the this one. 302 * The only diffrence may be extra alt and draft attributes but the 303 * value of type attribute is the same 304 * 305 * @param last 306 * @return 307 */ isLike(XPathParts last)308 public boolean isLike(XPathParts last) { 309 int min = elements.size(); 310 if (last.elements.size() < min) min = last.elements.size(); 311 for (int i = 0; i < min; ++i) { 312 Element e1 = elements.get(i); 313 Element e2 = last.elements.get(i); 314 if (!e1.equals(e2)) { 315 /* is the current element the last one */ 316 if (i == min - 1) { 317 String et1 = e1.getAttributeValue("type"); 318 String et2 = e2.getAttributeValue("type"); 319 if (et1 == null && et2 == null) { 320 et1 = e1.getAttributeValue("id"); 321 et2 = e2.getAttributeValue("id"); 322 } 323 if (et1 != null && et2 != null && et1.equals(et2)) { 324 return true; 325 } 326 } else { 327 return false; 328 } 329 } 330 } 331 return false; 332 } 333 334 /** 335 * Does this xpath contain the attribute at all? 336 */ containsAttribute(String attribute)337 public boolean containsAttribute(String attribute) { 338 for (int i = 0; i < elements.size(); ++i) { 339 Element element = elements.get(i); 340 if (element.getAttributeValue(attribute) != null) { 341 return true; 342 } 343 } 344 return false; 345 } 346 347 /** 348 * Does it contain the attribute/value pair? 349 */ containsAttributeValue(String attribute, String value)350 public boolean containsAttributeValue(String attribute, String value) { 351 for (int i = 0; i < elements.size(); ++i) { 352 String otherValue = elements.get(i).getAttributeValue(attribute); 353 if (otherValue != null && value.equals(otherValue)) return true; 354 } 355 return false; 356 } 357 358 /** 359 * How many elements are in this xpath? 360 */ size()361 public int size() { 362 return elements.size(); 363 } 364 365 /** 366 * Get the nth element. Negative values are from end 367 */ getElement(int elementIndex)368 public String getElement(int elementIndex) { 369 return elements.get(elementIndex >= 0 ? elementIndex : elementIndex + size()).getElement(); 370 } 371 getAttributeCount(int elementIndex)372 public int getAttributeCount(int elementIndex) { 373 return elements.get(elementIndex >= 0 ? elementIndex : elementIndex + size()).getAttributeCount(); 374 } 375 376 /** 377 * Get the attributes for the nth element (negative index is from end). Returns null or an empty map if there's 378 * nothing. 379 * PROBLEM: exposes internal map 380 */ getAttributes(int elementIndex)381 public Map<String, String> getAttributes(int elementIndex) { 382 return elements.get(elementIndex >= 0 ? elementIndex : elementIndex + size()).getAttributes(); 383 } 384 385 /** 386 * return non-modifiable collection 387 * 388 * @param elementIndex 389 * @return 390 */ getAttributeKeys(int elementIndex)391 public Collection<String> getAttributeKeys(int elementIndex) { 392 return elements.get(elementIndex >= 0 ? elementIndex : elementIndex + size()) 393 .getAttributes() 394 .keySet(); 395 } 396 397 /** 398 * Get the attributeValue for the attrbute at the nth element (negative index is from end). Returns null if there's 399 * nothing. 400 */ getAttributeValue(int elementIndex, String attribute)401 public String getAttributeValue(int elementIndex, String attribute) { 402 if (elementIndex < 0) { 403 elementIndex += size(); 404 } 405 return elements.get(elementIndex).getAttributeValue(attribute); 406 } 407 putAttributeValue(int elementIndex, String attribute, String value)408 public void putAttributeValue(int elementIndex, String attribute, String value) { 409 elements.get(elementIndex >= 0 ? elementIndex : elementIndex + size()).putAttribute(attribute, value); 410 } 411 412 /** 413 * Get the attributes for the nth element. Returns null or an empty map if there's nothing. 414 * PROBLEM: exposes internal map 415 */ findAttributes(String elementName)416 public Map<String, String> findAttributes(String elementName) { 417 int index = findElement(elementName); 418 if (index == -1) { 419 return null; 420 } 421 return getAttributes(index); 422 } 423 424 /** 425 * Find the attribute value 426 */ findAttributeValue(String elementName, String attributeName)427 public String findAttributeValue(String elementName, String attributeName) { 428 Map<String, String> attributes = findAttributes(elementName); 429 if (attributes == null) { 430 return null; 431 } 432 return (String) attributes.get(attributeName); 433 } 434 435 /** 436 * Add an Element object to this XPathParts, using the given element name. 437 * If this is the first Element in this XPathParts, also set dtdData. 438 * Do not set any attributes. 439 * 440 * @param element the string describing the element, such as "ldml", 441 * "supplementalData", etc. 442 * @return this XPathParts 443 */ addElement(String element)444 public XPathParts addElement(String element) { 445 if (elements.size() == 0) { 446 try { 447 /* 448 * The first element should match one of the DtdType enum values. 449 * Use it to set dtdData. 450 */ 451 dtdData = DtdData.getInstance(DtdType.valueOf(element)); 452 } catch (Exception e) { 453 dtdData = null; 454 } 455 } 456 elements.add(new Element(element)); 457 return this; 458 } 459 460 /** 461 * Varargs version of addElement. 462 * Usage: xpp.addElements("ldml","localeDisplayNames") 463 * @param element 464 * @return this for chaining 465 */ addElements(String... element)466 public XPathParts addElements(String... element) { 467 for (String e : element) { 468 addElement(e); 469 } 470 return this; 471 } 472 473 /** 474 * Add an attribute/value pair to the current last element. 475 */ addAttribute(String attribute, String value)476 public XPathParts addAttribute(String attribute, String value) { 477 Element e = elements.get(elements.size() - 1); 478 e.putAttribute(attribute, value); 479 return this; 480 } 481 removeAttribute(String elementName, String attributeName)482 public XPathParts removeAttribute(String elementName, String attributeName) { 483 return removeAttribute(findElement(elementName), attributeName); 484 } 485 removeAttribute(int elementIndex, String attributeName)486 public XPathParts removeAttribute(int elementIndex, String attributeName) { 487 elements.get(elementIndex >= 0 ? elementIndex : elementIndex + size()).putAttribute(attributeName, null); 488 return this; 489 } 490 removeAttributes(String elementName, Collection<String> attributeNames)491 public XPathParts removeAttributes(String elementName, Collection<String> attributeNames) { 492 return removeAttributes(findElement(elementName), attributeNames); 493 } 494 removeAttributes(int elementIndex, Collection<String> attributeNames)495 public XPathParts removeAttributes(int elementIndex, Collection<String> attributeNames) { 496 elements.get(elementIndex >= 0 ? elementIndex : elementIndex + size()).removeAttributes(attributeNames); 497 return this; 498 } 499 500 /** 501 * Parse out an xpath, and pull in the elements and attributes. 502 * 503 * @param xPath 504 * @return this XPathParts 505 * 506 * This is only for use by CLDRFile.write(), which is for generating vxml, etc., using defaultSuppressionMap. 507 * 508 * All other functions that would have called XPathParts.set() in the past should now use getInstance or getFrozenInstance 509 * instead, to take advantage of caching. 510 * Reference: https://unicode-org.atlassian.net/browse/CLDR-12007 511 */ setForWritingWithSuppressionMap(String xPath)512 public XPathParts setForWritingWithSuppressionMap(String xPath) { 513 if (frozen) { 514 throw new UnsupportedOperationException("Can't modify frozen Element"); 515 } 516 return addInternal(xPath, true); 517 } 518 519 /** 520 * Add the given path to this XPathParts. 521 * 522 * @param xPath the path string 523 * @param initial boolean, if true, call elements.clear() and set dtdData = null before adding, 524 * and make requiredPrefix // instead of / 525 * @return the XPathParts, or parseError 526 * 527 * Called by set (initial = true), and addRelative (initial = false) 528 */ addInternal(String xPath, boolean initial)529 private XPathParts addInternal(String xPath, boolean initial) { 530 String lastAttributeName = ""; 531 String requiredPrefix = "/"; 532 if (initial) { 533 elements.clear(); 534 dtdData = null; 535 requiredPrefix = "//"; 536 } 537 if (!xPath.startsWith(requiredPrefix)) { 538 return parseError(xPath, 0); 539 } 540 int stringStart = requiredPrefix.length(); // skip prefix 541 char state = 'p'; 542 // since only ascii chars are relevant, use char 543 int len = xPath.length(); 544 for (int i = 2; i < len; ++i) { 545 char cp = xPath.charAt(i); 546 if (cp != state && (state == '\"' || state == '\'')) { 547 continue; // stay in quotation 548 } 549 switch (cp) { 550 case '/': 551 if (state != 'p' || stringStart >= i) { 552 return parseError(xPath, i); 553 } 554 if (stringStart > 0) { 555 addElement(xPath.substring(stringStart, i)); 556 } 557 stringStart = i + 1; 558 break; 559 case '[': 560 if (state != 'p' || stringStart >= i) { 561 return parseError(xPath, i); 562 } 563 if (stringStart > 0) { 564 addElement(xPath.substring(stringStart, i)); 565 } 566 state = cp; 567 break; 568 case '@': 569 if (state != '[') { 570 return parseError(xPath, i); 571 } 572 stringStart = i + 1; 573 state = cp; 574 break; 575 case '=': 576 if (state != '@' || stringStart >= i) { 577 return parseError(xPath, i); 578 } 579 lastAttributeName = xPath.substring(stringStart, i); 580 state = cp; 581 break; 582 case '\"': 583 case '\'': 584 if (state == cp) { // finished 585 if (stringStart > i) { 586 return parseError(xPath, i); 587 } 588 addAttribute(lastAttributeName, xPath.substring(stringStart, i)); 589 state = 'e'; 590 break; 591 } 592 if (state != '=') { 593 return parseError(xPath, i); 594 } 595 stringStart = i + 1; 596 state = cp; 597 break; 598 case ']': 599 if (state != 'e') { 600 return parseError(xPath, i); 601 } 602 state = 'p'; 603 stringStart = -1; 604 break; 605 } 606 } 607 // check to make sure terminated 608 if (state != 'p' || stringStart >= xPath.length()) { 609 return parseError(xPath, xPath.length()); 610 } 611 if (stringStart > 0) { 612 addElement(xPath.substring(stringStart, xPath.length())); 613 } 614 return this; 615 } 616 617 /** 618 * boilerplate 619 */ toString()620 public String toString() { 621 return toString(elements.size()); 622 } 623 toString(int limit)624 public String toString(int limit) { 625 if (limit < 0) { 626 limit += size(); 627 } 628 String result = "/"; 629 try { 630 for (int i = 0; i < limit; ++i) { 631 result += elements.get(i).toString(XPATH_STYLE); 632 } 633 } catch (RuntimeException e) { 634 throw e; 635 } 636 return result; 637 } 638 toString(int start, int limit)639 public String toString(int start, int limit) { 640 if (start < 0) { 641 start += size(); 642 } 643 if (limit < 0) { 644 limit += size(); 645 } 646 String result = ""; 647 for (int i = start; i < limit; ++i) { 648 result += elements.get(i).toString(XPATH_STYLE); 649 } 650 return result; 651 } 652 653 /** 654 * boilerplate 655 */ equals(Object other)656 public boolean equals(Object other) { 657 try { 658 XPathParts that = (XPathParts) other; 659 if (elements.size() != that.elements.size()) return false; 660 for (int i = 0; i < elements.size(); ++i) { 661 if (!elements.get(i).equals(that.elements.get(i))) { 662 return false; 663 } 664 } 665 return true; 666 } catch (ClassCastException e) { 667 return false; 668 } 669 } 670 671 /** 672 * boilerplate 673 */ hashCode()674 public int hashCode() { 675 int result = elements.size(); 676 for (int i = 0; i < elements.size(); ++i) { 677 result = result * 37 + elements.get(i).hashCode(); 678 } 679 return result; 680 } 681 682 // ========== Privates ========== 683 parseError(String s, int i)684 private XPathParts parseError(String s, int i) { 685 throw new IllegalArgumentException("Malformed xPath '" + s + "' at " + i); 686 } 687 688 public static final int XPATH_STYLE = 0, XML_OPEN = 1, XML_CLOSE = 2, XML_NO_VALUE = 3; 689 public static final String NEWLINE = "\n"; 690 691 private final class Element implements Cloneable, Freezable<Element> { 692 private volatile boolean frozen; 693 private final String element; 694 private Map<String, String> attributes; // = new TreeMap(AttributeComparator); 695 Element(String element)696 public Element(String element) { 697 this(element, null); 698 } 699 Element(Element other, String element)700 public Element(Element other, String element) { 701 this(element, other.attributes); 702 } 703 Element(String element, Map<String, String> attributes)704 public Element(String element, Map<String, String> attributes) { 705 this.frozen = false; 706 this.element = element.intern(); // allow fast comparison 707 if (attributes == null) { 708 this.attributes = null; 709 } else { 710 this.attributes = new TreeMap<String, String>(getAttributeComparator()); 711 this.attributes.putAll(attributes); 712 } 713 } 714 715 @Override clone()716 protected Object clone() throws CloneNotSupportedException { 717 return frozen ? this 718 : new Element(element, attributes); 719 } 720 721 /** 722 * Add the given attribute, value pair to this Element object; or, 723 * if value is null, remove the attribute. 724 * 725 * @param attribute, the string such as "number" or "cldrVersion" 726 * @param value, the string such as "$Revision$" or "35", or null for removal 727 */ putAttribute(String attribute, String value)728 public void putAttribute(String attribute, String value) { 729 attribute = attribute.intern(); // allow fast comparison 730 if (frozen) { 731 throw new UnsupportedOperationException("Can't modify frozen object."); 732 } 733 if (value == null) { 734 if (attributes != null) { 735 attributes.remove(attribute); 736 if (attributes.size() == 0) { 737 attributes = null; 738 } 739 } 740 } else { 741 if (attributes == null) { 742 attributes = new TreeMap<String, String>(getAttributeComparator()); 743 } 744 attributes.put(attribute, value); 745 } 746 } 747 748 /** 749 * Remove the given attributes from this Element object. 750 * 751 * @param attributeNames 752 */ removeAttributes(Collection<String> attributeNames)753 private void removeAttributes(Collection<String> attributeNames) { 754 if (frozen) { 755 throw new UnsupportedOperationException("Can't modify frozen object."); 756 } 757 if (attributeNames == null) { 758 return; 759 } 760 for (String attribute : attributeNames) { 761 attributes.remove(attribute); 762 } 763 if (attributes.size() == 0) { 764 attributes = null; 765 } 766 } 767 toString()768 public String toString() { 769 throw new IllegalArgumentException("Don't use"); 770 } 771 772 /** 773 * @param style 774 * from XPATH_STYLE 775 * @return 776 */ toString(int style)777 public String toString(int style) { 778 StringBuilder result = new StringBuilder(); 779 // Set keys; 780 switch (style) { 781 case XPathParts.XPATH_STYLE: 782 result.append('/').append(element); 783 writeAttributes("[@", "\"]", false, result); 784 break; 785 case XPathParts.XML_OPEN: 786 case XPathParts.XML_NO_VALUE: 787 result.append('<').append(element); 788 writeAttributes(" ", "\"", true, result); 789 if (style == XML_NO_VALUE) { 790 result.append('/'); 791 } 792 if (CLDRFile.HACK_ORDER && element.equals("ldml")) { 793 result.append(' '); 794 } 795 result.append('>'); 796 break; 797 case XML_CLOSE: 798 result.append("</").append(element).append('>'); 799 break; 800 } 801 return result.toString(); 802 } 803 804 /** 805 * @param element 806 * TODO 807 * @param prefix 808 * TODO 809 * @param postfix 810 * TODO 811 * @param removeLDMLExtras 812 * TODO 813 * @param result 814 */ writeAttributes(String prefix, String postfix, boolean removeLDMLExtras, StringBuilder result)815 private Element writeAttributes(String prefix, String postfix, 816 boolean removeLDMLExtras, StringBuilder result) { 817 if (getAttributeCount() == 0) { 818 return this; 819 } 820 for (Entry<String, String> attributesAndValues : attributes.entrySet()) { 821 String attribute = attributesAndValues.getKey(); 822 String value = attributesAndValues.getValue(); 823 if (removeLDMLExtras && suppressionMap != null) { 824 if (skipAttribute(element, attribute, value)) { 825 continue; 826 } 827 if (skipAttribute("*", attribute, value)) { 828 continue; 829 } 830 } 831 try { 832 result.append(prefix).append(attribute).append("=\"") 833 .append(removeLDMLExtras ? TransliteratorUtilities.toHTML.transliterate(value) : value) 834 .append(postfix); 835 } catch (RuntimeException e) { 836 throw e; // for debugging 837 } 838 } 839 return this; 840 } 841 842 /** 843 * Should writeAttributes skip the given element, attribute, and value? 844 * 845 * @param element 846 * @param attribute 847 * @param value 848 * @return true to skip, else false 849 * 850 * Called only by writeAttributes 851 * 852 * Assume suppressionMap isn't null. 853 */ skipAttribute(String element, String attribute, String value)854 private boolean skipAttribute(String element, String attribute, String value) { 855 Map<String, String> attribute_value = suppressionMap.get(element); 856 boolean skip = false; 857 if (attribute_value != null) { 858 Object suppressValue = attribute_value.get(attribute); 859 if (suppressValue == null) { 860 suppressValue = attribute_value.get("*"); 861 } 862 if (suppressValue != null) { 863 if (value.equals(suppressValue) || suppressValue.equals("*")) { 864 skip = true; 865 } 866 } 867 } 868 return skip; 869 } 870 equals(Object other)871 public boolean equals(Object other) { 872 if (other == null) { 873 return false; 874 } 875 try { 876 Element that = (Element) other; 877 // == check is ok since we intern elements 878 return element == that.element 879 && (attributes == null ? that.attributes == null 880 : that.attributes == null ? attributes == null 881 : attributes.equals(that.attributes)); 882 } catch (ClassCastException e) { 883 return false; 884 } 885 } 886 hashCode()887 public int hashCode() { 888 return element.hashCode() * 37 + (attributes == null ? 0 : attributes.hashCode()); 889 } 890 getElement()891 public String getElement() { 892 return element; 893 } 894 getAttributeCount()895 private int getAttributeCount() { 896 if (attributes == null) { 897 return 0; 898 } 899 return attributes.size(); 900 } 901 getAttributes()902 private Map<String, String> getAttributes() { 903 if (attributes == null) { 904 return Collections.emptyMap(); 905 } 906 return Collections.unmodifiableMap(attributes); 907 } 908 getAttributeValue(String attribute)909 private String getAttributeValue(String attribute) { 910 if (attributes == null) { 911 return null; 912 } 913 return attributes.get(attribute); 914 } 915 916 @Override isFrozen()917 public boolean isFrozen() { 918 return frozen; 919 } 920 921 @Override freeze()922 public Element freeze() { 923 if (!frozen) { 924 attributes = attributes == null ? null 925 : Collections.unmodifiableMap(attributes); 926 frozen = true; 927 } 928 return this; 929 } 930 931 @Override cloneAsThawed()932 public Element cloneAsThawed() { 933 return new Element(element, attributes); 934 } 935 } 936 937 /** 938 * Search for an element within the path. 939 * 940 * @param elementName 941 * the element to look for 942 * @return element number if found, else -1 if not found 943 */ findElement(String elementName)944 public int findElement(String elementName) { 945 for (int i = 0; i < elements.size(); ++i) { 946 Element e = elements.get(i); 947 if (!e.getElement().equals(elementName)) { 948 continue; 949 } 950 return i; 951 } 952 return -1; 953 } 954 955 /** 956 * Get the MapComparator for this XPathParts. 957 * 958 * @return the MapComparator, or null 959 * 960 * Called by the Element constructor, and by putAttribute 961 */ getAttributeComparator()962 private MapComparator<String> getAttributeComparator() { 963 return dtdData == null ? null 964 : dtdData.dtdType == DtdType.ldml ? CLDRFile.getAttributeOrdering() 965 : dtdData.getAttributeComparator(); 966 } 967 968 /** 969 * Determines if an elementName is contained in the path. 970 * 971 * @param elementName 972 * @return 973 */ contains(String elementName)974 public boolean contains(String elementName) { 975 return findElement(elementName) >= 0; 976 } 977 978 /** 979 * add a relative path to this XPathParts. 980 */ addRelative(String path)981 public XPathParts addRelative(String path) { 982 if (frozen) { 983 throw new UnsupportedOperationException("Can't modify frozen Element"); 984 } 985 if (path.startsWith("//")) { 986 elements.clear(); 987 path = path.substring(1); // strip one 988 } else { 989 while (path.startsWith("../")) { 990 path = path.substring(3); 991 trimLast(); 992 } 993 if (!path.startsWith("/")) path = "/" + path; 994 } 995 return addInternal(path, false); 996 } 997 998 /** 999 */ trimLast()1000 public XPathParts trimLast() { 1001 if (frozen) { 1002 throw new UnsupportedOperationException("Can't modify frozen Element"); 1003 } 1004 elements.remove(elements.size() - 1); 1005 return this; 1006 } 1007 1008 /** 1009 * Replace the elements of this XPathParts with clones of the elements of the given other XPathParts 1010 * 1011 * @param parts the given other XPathParts (not modified) 1012 * @return this XPathParts (modified) 1013 * 1014 * Called by XPathParts.replace and CldrItem.split. 1015 */ set(XPathParts parts)1016 public XPathParts set(XPathParts parts) { 1017 if (frozen) { 1018 throw new UnsupportedOperationException("Can't modify frozen Element"); 1019 } 1020 try { 1021 dtdData = parts.dtdData; 1022 elements.clear(); 1023 for (Element element : parts.elements) { 1024 elements.add((Element) element.clone()); 1025 } 1026 return this; 1027 } catch (CloneNotSupportedException e) { 1028 throw (InternalError) new InternalError().initCause(e); 1029 } 1030 } 1031 1032 /** 1033 * Replace up to i with parts 1034 * 1035 * @param i 1036 * @param parts 1037 */ replace(int i, XPathParts parts)1038 public XPathParts replace(int i, XPathParts parts) { 1039 if (frozen) { 1040 throw new UnsupportedOperationException("Can't modify frozen Element"); 1041 } 1042 List<Element> temp = elements; 1043 elements = new ArrayList<Element>(); 1044 set(parts); 1045 for (; i < temp.size(); ++i) { 1046 elements.add(temp.get(i)); 1047 } 1048 return this; 1049 } 1050 1051 /** 1052 * Utility to write a comment. 1053 * 1054 * @param pw 1055 * @param blockComment 1056 * TODO 1057 * @param indent 1058 */ writeComment(PrintWriter pw, int indent, String comment, boolean blockComment)1059 static void writeComment(PrintWriter pw, int indent, String comment, boolean blockComment) { 1060 // now write the comment 1061 if (comment.length() == 0) return; 1062 if (blockComment) { 1063 pw.print(Utility.repeat("\t", indent)); 1064 } else { 1065 pw.print(" "); 1066 } 1067 pw.print("<!--"); 1068 if (comment.indexOf(NEWLINE) > 0) { 1069 boolean first = true; 1070 int countEmptyLines = 0; 1071 // trim the line iff the indent != 0. 1072 for (Iterator<String> it = CldrUtility.splitList(comment, NEWLINE, indent != 0, null).iterator(); it.hasNext();) { 1073 String line = it.next(); 1074 if (line.length() == 0) { 1075 ++countEmptyLines; 1076 continue; 1077 } 1078 if (countEmptyLines != 0) { 1079 for (int i = 0; i < countEmptyLines; ++i) 1080 pw.println(); 1081 countEmptyLines = 0; 1082 } 1083 if (first) { 1084 first = false; 1085 line = line.trim(); 1086 pw.print(" "); 1087 } else if (indent != 0) { 1088 pw.print(Utility.repeat("\t", (indent + 1))); 1089 pw.print(" "); 1090 } 1091 pw.println(line); 1092 } 1093 pw.print(Utility.repeat("\t", indent)); 1094 } else { 1095 pw.print(" "); 1096 pw.print(comment.trim()); 1097 pw.print(" "); 1098 } 1099 pw.print("-->"); 1100 if (blockComment) { 1101 pw.println(); 1102 } 1103 } 1104 1105 /** 1106 * Utility to determine if this a language locale? 1107 * Note: a script is included with the language, if there is one. 1108 * 1109 * @param in 1110 * @return 1111 */ isLanguage(String in)1112 public static boolean isLanguage(String in) { 1113 int pos = in.indexOf('_'); 1114 if (pos < 0) return true; 1115 if (in.indexOf('_', pos + 1) >= 0) return false; // no more than 2 subtags 1116 if (in.length() != pos + 5) return false; // second must be 4 in length 1117 return true; 1118 } 1119 1120 /** 1121 * Returns -1 if parent isn't really a parent, 0 if they are identical, and 1 if parent is a proper parent 1122 */ isSubLocale(String parent, String possibleSublocale)1123 public static int isSubLocale(String parent, String possibleSublocale) { 1124 if (parent.equals("root")) { 1125 if (parent.equals(possibleSublocale)) return 0; 1126 return 1; 1127 } 1128 if (parent.length() > possibleSublocale.length()) return -1; 1129 if (!possibleSublocale.startsWith(parent)) return -1; 1130 if (parent.length() == possibleSublocale.length()) return 0; 1131 if (possibleSublocale.charAt(parent.length()) != '_') return -1; // last subtag too long 1132 return 1; 1133 } 1134 1135 /** 1136 * Sets an attribute/value on the first matching element. 1137 */ setAttribute(String elementName, String attributeName, String attributeValue)1138 public XPathParts setAttribute(String elementName, String attributeName, String attributeValue) { 1139 int index = findElement(elementName); 1140 elements.get(index).putAttribute(attributeName, attributeValue); 1141 return this; 1142 } 1143 removeProposed()1144 public XPathParts removeProposed() { 1145 for (int i = 0; i < elements.size(); ++i) { 1146 Element element = elements.get(i); 1147 if (element.getAttributeCount() == 0) { 1148 continue; 1149 } 1150 for (Entry<String, String> attributesAndValues : element.getAttributes().entrySet()) { 1151 String attribute = attributesAndValues.getKey(); 1152 if (!attribute.equals("alt")) { 1153 continue; 1154 } 1155 String attributeValue = attributesAndValues.getValue(); 1156 int pos = attributeValue.indexOf("proposed"); 1157 if (pos < 0) break; 1158 if (pos > 0 && attributeValue.charAt(pos - 1) == '-') --pos; // backup for "...-proposed" 1159 if (pos == 0) { 1160 element.putAttribute(attribute, null); 1161 break; 1162 } 1163 attributeValue = attributeValue.substring(0, pos); // strip it off 1164 element.putAttribute(attribute, attributeValue); 1165 break; // there is only one alt! 1166 } 1167 } 1168 return this; 1169 } 1170 setElement(int elementIndex, String newElement)1171 public XPathParts setElement(int elementIndex, String newElement) { 1172 if (elementIndex < 0) { 1173 elementIndex += size(); 1174 } 1175 Element element = elements.get(elementIndex); 1176 elements.set(elementIndex, new Element(element, newElement)); 1177 return this; 1178 } 1179 removeElement(int elementIndex)1180 public XPathParts removeElement(int elementIndex) { 1181 elements.remove(elementIndex >= 0 ? elementIndex : elementIndex + size()); 1182 return this; 1183 } 1184 findFirstAttributeValue(String attribute)1185 public String findFirstAttributeValue(String attribute) { 1186 for (int i = 0; i < elements.size(); ++i) { 1187 String value = getAttributeValue(i, attribute); 1188 if (value != null) { 1189 return value; 1190 } 1191 } 1192 return null; 1193 } 1194 setAttribute(int elementIndex, String attributeName, String attributeValue)1195 public void setAttribute(int elementIndex, String attributeName, String attributeValue) { 1196 Element element = elements.get(elementIndex >= 0 ? elementIndex : elementIndex + size()); 1197 element.putAttribute(attributeName, attributeValue); 1198 } 1199 1200 @Override isFrozen()1201 public boolean isFrozen() { 1202 return frozen; 1203 } 1204 1205 @Override freeze()1206 public XPathParts freeze() { 1207 if (!frozen) { 1208 // ensure that it can't be modified. Later we can fix all the call sites to check frozen. 1209 List<Element> temp = new ArrayList<>(elements.size()); 1210 for (Element element : elements) { 1211 temp.add(element.freeze()); 1212 } 1213 elements = Collections.unmodifiableList(temp); 1214 frozen = true; 1215 } 1216 return this; 1217 } 1218 1219 @Override cloneAsThawed()1220 public XPathParts cloneAsThawed() { 1221 XPathParts xppClone = new XPathParts(); 1222 /* 1223 * Remember to copy dtdData. 1224 * Reference: https://unicode.org/cldr/trac/ticket/12007 1225 */ 1226 xppClone.dtdData = this.dtdData; 1227 xppClone.suppressionMap = this.suppressionMap; 1228 for (Element e : this.elements) { 1229 xppClone.elements.add(e.cloneAsThawed()); 1230 } 1231 return xppClone; 1232 } 1233 getFrozenInstance(String path)1234 public static synchronized XPathParts getFrozenInstance(String path) { 1235 XPathParts result = cache.get(path); 1236 if (result == null) { 1237 result = new XPathParts().addInternal(path, true).freeze(); 1238 cache.put(path, result); 1239 } 1240 return result; 1241 } 1242 getInstance(String path)1243 public static XPathParts getInstance(String path) { 1244 return getFrozenInstance(path).cloneAsThawed(); 1245 } 1246 getDtdData()1247 public DtdData getDtdData() { 1248 return dtdData; 1249 } 1250 getElements()1251 public Set<String> getElements() { 1252 Builder<String> builder = ImmutableSet.builder(); 1253 for (int i = 0; i < elements.size(); ++i) { 1254 builder.add(elements.get(i).getElement()); 1255 } 1256 return builder.build(); 1257 } 1258 getSpecialNondistinguishingAttributes()1259 public Map<String, String> getSpecialNondistinguishingAttributes() { 1260 Map<String, String> ueMap = null; // common case, none found. 1261 for (int i = 0; i < this.size(); i++) { 1262 // taken from XPathTable.getUndistinguishingElementsFor, with some cleanup 1263 // from XPathTable.getUndistinguishingElements, we include alt, draft 1264 for (Entry<String, String> entry : this.getAttributes(i).entrySet()) { 1265 String k = entry.getKey(); 1266 if (getDtdData().isDistinguishing(getElement(i), k) 1267 || k.equals("alt") // is always distinguishing, so we don't really need this. 1268 || k.equals("draft")) { 1269 continue; 1270 } 1271 if (ueMap == null) { 1272 ueMap = new TreeMap<String, String>(); 1273 } 1274 ueMap.put(k, entry.getValue()); 1275 } 1276 } 1277 return ueMap; 1278 } 1279 } 1280