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