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 public final class XPathParts extends XPathParser 39 implements Freezable<XPathParts>, Comparable<XPathParts> { 40 private static final boolean DEBUGGING = false; 41 42 private volatile boolean frozen = false; 43 44 private List<Element> elements = new ArrayList<>(); 45 46 private DtdData dtdData = null; 47 48 private static final Map<String, XPathParts> cache = new ConcurrentHashMap<>(); 49 50 /** 51 * Construct a new empty XPathParts object. 52 * 53 * <p>Note: for faster performance, call getFrozenInstance or getInstance instead of this 54 * constructor. This constructor remains public for special cases in which individual elements 55 * are added with addElement rather than using a complete path string. 56 */ XPathParts()57 public XPathParts() {} 58 59 /** See if the xpath contains an element */ containsElement(String element)60 public boolean containsElement(String element) { 61 for (int i = 0; i < elements.size(); ++i) { 62 if (elements.get(i).getElement().equals(element)) { 63 return true; 64 } 65 } 66 return false; 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? */ containsAttribute(String attribute)365 public boolean containsAttribute(String attribute) { 366 for (int i = 0; i < elements.size(); ++i) { 367 Element element = elements.get(i); 368 if (element.getAttributeValue(attribute) != null) { 369 return true; 370 } 371 } 372 return false; 373 } 374 375 /** Does it contain the attribute/value pair? */ containsAttributeValue(String attribute, String value)376 public boolean containsAttributeValue(String attribute, String value) { 377 for (int i = 0; i < elements.size(); ++i) { 378 String otherValue = elements.get(i).getAttributeValue(attribute); 379 if (otherValue != null && value.equals(otherValue)) return true; 380 } 381 return false; 382 } 383 384 /** How many elements are in this xpath? */ size()385 public int size() { 386 return elements.size(); 387 } 388 389 /** Get the nth element. Negative values are from end */ 390 @Override getElement(int elementIndex)391 public String getElement(int elementIndex) { 392 return elements.get(elementIndex >= 0 ? elementIndex : elementIndex + size()).getElement(); 393 } 394 getAttributeCount(int elementIndex)395 public int getAttributeCount(int elementIndex) { 396 return elements.get(elementIndex >= 0 ? elementIndex : elementIndex + size()) 397 .getAttributeCount(); 398 } 399 400 /** 401 * Get the attributes for the nth element (negative index is from end). Returns null or an empty 402 * map if there's nothing. PROBLEM: exposes internal map 403 */ getAttributes(int elementIndex)404 public Map<String, String> getAttributes(int elementIndex) { 405 return elements.get(elementIndex >= 0 ? elementIndex : elementIndex + size()) 406 .getAttributes(); 407 } 408 409 /** 410 * return non-modifiable collection 411 * 412 * @param elementIndex 413 * @return 414 */ getAttributeKeys(int elementIndex)415 public Collection<String> getAttributeKeys(int elementIndex) { 416 return elements.get(elementIndex >= 0 ? elementIndex : elementIndex + size()) 417 .getAttributes() 418 .keySet(); 419 } 420 421 /** 422 * Get the attributeValue for the attrbute at the nth element (negative index is from end). 423 * Returns null if there's nothing. 424 */ 425 @Override getAttributeValue(int elementIndex, String attribute)426 public String getAttributeValue(int elementIndex, String attribute) { 427 if (elementIndex < 0) { 428 elementIndex += size(); 429 } 430 return elements.get(elementIndex).getAttributeValue(attribute); 431 } 432 putAttributeValue(int elementIndex, String attribute, String value)433 public void putAttributeValue(int elementIndex, String attribute, String value) { 434 elementIndex = elementIndex >= 0 ? elementIndex : elementIndex + size(); 435 Map<String, String> ea = elements.get(elementIndex).attributes; 436 if (value == null && (ea == null || !ea.containsKey(attribute))) { 437 return; 438 } 439 if (value != null && ea != null && value.equals(ea.get(attribute))) { 440 return; 441 } 442 makeElementsMutable(); 443 makeElementMutable(elementIndex); 444 // make mutable may change elements.get(elementIndex), so we have to use 445 // elements.get(elementIndex) after calling 446 elements.get(elementIndex).putAttribute(attribute, value); 447 } 448 449 /** 450 * Get the attributes for the nth element. Returns null or an empty map if there's nothing. 451 * PROBLEM: exposes internal map 452 */ findAttributes(String elementName)453 public Map<String, String> findAttributes(String elementName) { 454 int index = findElement(elementName); 455 if (index == -1) { 456 return null; 457 } 458 return getAttributes(index); 459 } 460 461 /** Find the attribute value */ findAttributeValue(String elementName, String attributeName)462 public String findAttributeValue(String elementName, String attributeName) { 463 Map<String, String> attributes = findAttributes(elementName); 464 if (attributes == null) { 465 return null; 466 } 467 return attributes.get(attributeName); 468 } 469 470 @Override handleClearElements()471 protected void handleClearElements() { 472 elements.clear(); 473 } 474 475 @Override handleAddElement(String element)476 protected void handleAddElement(String element) { 477 addElement(element); 478 } 479 /** 480 * Add an Element object to this XPathParts, using the given element name. If this is the first 481 * Element in this XPathParts, also set dtdData. Do not set any attributes. 482 * 483 * @param element the string describing the element, such as "ldml", "supplementalData", etc. 484 * @return this XPathParts 485 */ addElement(String element)486 public XPathParts addElement(String element) { 487 if (elements.size() == 0) { 488 try { 489 /* 490 * The first element should match one of the DtdType enum values. 491 * Use it to set dtdData. 492 */ 493 File dir = CLDRConfig.getInstance().getCldrBaseDirectory(); 494 dtdData = DtdData.getInstance(DtdType.fromElement(element), dir); 495 } catch (Exception e) { 496 dtdData = null; 497 } 498 } 499 makeElementsMutable(); 500 elements.add(new Element(element)); 501 return this; 502 } 503 makeElementsMutable()504 public void makeElementsMutable() { 505 if (frozen) { 506 throw new UnsupportedOperationException("Can't modify frozen object."); 507 } 508 509 if (elements instanceof ImmutableList) { 510 elements = new ArrayList<>(elements); 511 } 512 } 513 makeElementMutable(int elementIndex)514 public void makeElementMutable(int elementIndex) { 515 if (frozen) { 516 throw new UnsupportedOperationException("Can't modify frozen object."); 517 } 518 519 Element e = elements.get(elementIndex); 520 Map<String, String> ea = e.attributes; 521 if (ea == null || ea instanceof ImmutableMap) { 522 elements.set(elementIndex, e.cloneAsThawed()); 523 } 524 } 525 526 /** 527 * Varargs version of addElement. Usage: xpp.addElements("ldml","localeDisplayNames") 528 * 529 * @param element 530 * @return this for chaining 531 */ addElements(String... element)532 public XPathParts addElements(String... element) { 533 for (String e : element) { 534 addElement(e); 535 } 536 return this; 537 } 538 539 @Override handleAddAttribute(String attribute, String value)540 protected void handleAddAttribute(String attribute, String value) { 541 addAttribute(attribute, value); 542 } 543 544 /** Add an attribute/value pair to the current last element. */ addAttribute(String attribute, String value)545 public XPathParts addAttribute(String attribute, String value) { 546 putAttributeValue(elements.size() - 1, attribute, value); 547 return this; 548 } 549 removeAttribute(String elementName, String attributeName)550 public XPathParts removeAttribute(String elementName, String attributeName) { 551 return removeAttribute(findElement(elementName), attributeName); 552 } 553 removeAttribute(int elementIndex, String attributeName)554 public XPathParts removeAttribute(int elementIndex, String attributeName) { 555 putAttributeValue(elementIndex, attributeName, null); 556 return this; 557 } 558 removeAttributes(String elementName, Collection<String> attributeNames)559 public XPathParts removeAttributes(String elementName, Collection<String> attributeNames) { 560 return removeAttributes(findElement(elementName), attributeNames); 561 } 562 removeAttributes(int elementIndex, Collection<String> attributeNames)563 public XPathParts removeAttributes(int elementIndex, Collection<String> attributeNames) { 564 elementIndex = elementIndex >= 0 ? elementIndex : elementIndex + size(); 565 Map<String, String> ea = elements.get(elementIndex).attributes; 566 if (ea == null 567 || attributeNames == null 568 || attributeNames.isEmpty() 569 || Collections.disjoint(attributeNames, ea.keySet())) { 570 return this; 571 } 572 makeElementsMutable(); 573 makeElementMutable(elementIndex); 574 // make mutable may change elements.get(elementIndex), so we have to use 575 // elements.get(elementIndex) after calling 576 elements.get(elementIndex).removeAttributes(attributeNames); 577 return this; 578 } 579 580 /** 581 * Add the given path to this XPathParts. 582 * 583 * @param xPath the path string 584 * @param initial boolean, if true, call elements.clear() and set dtdData = null before adding, 585 * and make requiredPrefix // instead of / 586 * @return the XPathParts, or parseError 587 * <p>Called by set (initial = true), and addRelative (initial = false) 588 */ addInternal(String xPath, boolean initial)589 private XPathParts addInternal(String xPath, boolean initial) { 590 if (initial) { 591 dtdData = null; 592 } 593 594 // call superclass for parsing 595 handleParse(xPath, initial); 596 597 return this; 598 } 599 600 /** boilerplate */ 601 @Override toString()602 public String toString() { 603 return toString(elements.size()); 604 } 605 606 // TODO combine and optimize these 607 toString(int limit)608 public String toString(int limit) { 609 if (limit < 0) { 610 limit += size(); 611 } 612 String result = "/"; 613 try { 614 for (int i = 0; i < limit; ++i) { 615 result += elements.get(i).toString(XPATH_STYLE); 616 } 617 } catch (RuntimeException e) { 618 throw e; 619 } 620 return result; 621 } 622 toString(int start, int limit)623 public String toString(int start, int limit) { 624 if (start < 0) { 625 start += size(); 626 } 627 if (limit < 0) { 628 limit += size(); 629 } 630 StringBuilder result = new StringBuilder(); 631 for (int i = start; i < limit; ++i) { 632 result.append(elements.get(i).toString(XPATH_STYLE)); 633 } 634 return result.toString(); 635 } 636 637 /** boilerplate */ 638 @Override equals(Object other)639 public boolean equals(Object other) { 640 try { 641 XPathParts that = (XPathParts) other; 642 if (elements.size() != that.elements.size()) return false; 643 for (int i = 0; i < elements.size(); ++i) { 644 if (!elements.get(i).equals(that.elements.get(i))) { 645 return false; 646 } 647 } 648 return true; 649 } catch (ClassCastException e) { 650 return false; 651 } 652 } 653 654 @Override compareTo(XPathParts that)655 public int compareTo(XPathParts that) { 656 return dtdData.getDtdComparator().xpathComparator(this, that); 657 } 658 659 /** boilerplate */ 660 @Override hashCode()661 public int hashCode() { 662 int result = elements.size(); 663 for (int i = 0; i < elements.size(); ++i) { 664 result = result * 37 + elements.get(i).hashCode(); 665 } 666 return result; 667 } 668 669 // ========== Privates ========== 670 671 public static final int XPATH_STYLE = 0, XML_OPEN = 1, XML_CLOSE = 2, XML_NO_VALUE = 3; 672 public static final String NEWLINE = "\n"; 673 674 private final class Element { 675 private final String element; 676 private Map<String, String> attributes; // = new TreeMap(AttributeComparator); 677 Element(String element)678 public Element(String element) { 679 this(element, null); 680 } 681 Element(Element other, String element)682 public Element(Element other, String element) { 683 this(element, other.attributes); 684 } 685 Element(String element, Map<String, String> attributes)686 public Element(String element, Map<String, String> attributes) { 687 this.element = element.intern(); // allow fast comparison 688 if (attributes == null) { 689 this.attributes = null; 690 } else { 691 this.attributes = new TreeMap<>(getAttributeComparator()); 692 this.attributes.putAll(attributes); 693 } 694 } 695 696 /** 697 * Add the given attribute, value pair to this Element object; or, if value is null, remove 698 * the attribute. 699 * 700 * @param attribute, the string such as "number" or "cldrVersion" 701 * @param value, the string such as "$Revision$" or "35", or null for removal 702 */ putAttribute(String attribute, String value)703 public void putAttribute(String attribute, String value) { 704 attribute = attribute.intern(); // allow fast comparison 705 if (value == null) { 706 if (attributes != null) { 707 attributes.remove(attribute); 708 if (attributes.size() == 0) { 709 attributes = null; 710 } 711 } 712 } else { 713 if (attributes == null) { 714 attributes = new TreeMap<>(getAttributeComparator()); 715 } 716 attributes.put(attribute, value); 717 } 718 } 719 720 /** 721 * Remove the given attributes from this Element object. 722 * 723 * @param attributeNames 724 */ removeAttributes(Collection<String> attributeNames)725 private void removeAttributes(Collection<String> attributeNames) { 726 if (attributeNames == null) { 727 return; 728 } 729 for (String attribute : attributeNames) { 730 attributes.remove(attribute); 731 } 732 if (attributes.size() == 0) { 733 attributes = null; 734 } 735 } 736 737 @Override toString()738 public String toString() { 739 throw new IllegalArgumentException("Don't use"); 740 } 741 742 /** 743 * @param style from XPATH_STYLE 744 * @return 745 */ toString(int style)746 public String toString(int style) { 747 StringBuilder result = new StringBuilder(); 748 // Set keys; 749 switch (style) { 750 case XPathParts.XPATH_STYLE: 751 result.append('/').append(element); 752 writeAttributes("[@", "\"]", false, result); 753 break; 754 case XPathParts.XML_OPEN: 755 case XPathParts.XML_NO_VALUE: 756 result.append('<').append(element); 757 writeAttributes(" ", "\"", true, result); 758 if (style == XML_NO_VALUE) { 759 result.append('/'); 760 } 761 if (CLDRFile.HACK_ORDER && element.equals("ldml")) { 762 result.append(' '); 763 } 764 result.append('>'); 765 break; 766 case XML_CLOSE: 767 result.append("</").append(element).append('>'); 768 break; 769 } 770 return result.toString(); 771 } 772 773 /** 774 * @param element TODO 775 * @param prefix TODO 776 * @param postfix TODO 777 * @param removeLDMLExtras TODO 778 * @param result 779 */ writeAttributes( String prefix, String postfix, boolean removeLDMLExtras, StringBuilder result)780 private Element writeAttributes( 781 String prefix, String postfix, boolean removeLDMLExtras, StringBuilder result) { 782 if (getAttributeCount() == 0) { 783 return this; 784 } 785 Map<String, Map<String, String>> suppressionMap = null; 786 if (removeLDMLExtras) { 787 suppressionMap = CLDRFile.getDefaultSuppressionMap(); 788 } 789 for (Entry<String, String> attributesAndValues : attributes.entrySet()) { 790 String attribute = attributesAndValues.getKey(); 791 String value = attributesAndValues.getValue(); 792 if (removeLDMLExtras && suppressionMap != null) { 793 if (skipAttribute(element, attribute, value, suppressionMap)) { 794 continue; 795 } 796 if (skipAttribute("*", attribute, value, suppressionMap)) { 797 continue; 798 } 799 } 800 try { 801 result.append(prefix) 802 .append(attribute) 803 .append("=\"") 804 .append( 805 removeLDMLExtras 806 ? TransliteratorUtilities.toHTML.transliterate(value) 807 : value) 808 .append(postfix); 809 } catch (RuntimeException e) { 810 throw e; // for debugging 811 } 812 } 813 return this; 814 } 815 816 /** 817 * Should writeAttributes skip the given element, attribute, and value? 818 * 819 * @param element 820 * @param attribute 821 * @param value 822 * @return true to skip, else false 823 * <p>Called only by writeAttributes 824 * <p>Assume suppressionMap isn't null. 825 */ skipAttribute( String element, String attribute, String value, Map<String, Map<String, String>> suppressionMap)826 private boolean skipAttribute( 827 String element, 828 String attribute, 829 String value, 830 Map<String, Map<String, String>> suppressionMap) { 831 Map<String, String> attribute_value = suppressionMap.get(element); 832 boolean skip = false; 833 if (attribute_value != null) { 834 Object suppressValue = attribute_value.get(attribute); 835 if (suppressValue == null) { 836 suppressValue = attribute_value.get("*"); 837 } 838 if (suppressValue != null) { 839 if (value.equals(suppressValue) || suppressValue.equals("*")) { 840 skip = true; 841 } 842 } 843 } 844 return skip; 845 } 846 847 @Override equals(Object other)848 public boolean equals(Object other) { 849 if (other == null) { 850 return false; 851 } 852 try { 853 Element that = (Element) other; 854 // == check is ok since we intern elements 855 return element == that.element 856 && (attributes == null 857 ? that.attributes == null 858 : that.attributes == null 859 ? attributes == null 860 : attributes.equals(that.attributes)); 861 } catch (ClassCastException e) { 862 return false; 863 } 864 } 865 866 @Override hashCode()867 public int hashCode() { 868 return element.hashCode() * 37 + (attributes == null ? 0 : attributes.hashCode()); 869 } 870 getElement()871 public String getElement() { 872 return element; 873 } 874 getAttributeCount()875 private int getAttributeCount() { 876 if (attributes == null) { 877 return 0; 878 } 879 return attributes.size(); 880 } 881 getAttributes()882 private Map<String, String> getAttributes() { 883 if (attributes == null) { 884 return ImmutableMap.of(); 885 } 886 return ImmutableMap.copyOf(attributes); 887 } 888 getAttributeValue(String attribute)889 private String getAttributeValue(String attribute) { 890 if (attributes == null) { 891 return null; 892 } 893 return attributes.get(attribute); 894 } 895 makeImmutable()896 public Element makeImmutable() { 897 if (attributes != null && !(attributes instanceof ImmutableMap)) { 898 attributes = ImmutableMap.copyOf(attributes); 899 } 900 901 return this; 902 } 903 cloneAsThawed()904 public Element cloneAsThawed() { 905 return new Element(element, attributes); 906 } 907 } 908 909 /** 910 * Search for an element within the path. 911 * 912 * @param elementName the element to look for 913 * @return element number if found, else -1 if not found 914 */ findElement(String elementName)915 public int findElement(String elementName) { 916 for (int i = 0; i < elements.size(); ++i) { 917 Element e = elements.get(i); 918 if (!e.getElement().equals(elementName)) { 919 continue; 920 } 921 return i; 922 } 923 return -1; 924 } 925 926 /** 927 * Get the MapComparator for this XPathParts. 928 * 929 * @return the MapComparator, or null 930 * <p>Called by the Element constructor, and by putAttribute 931 */ getAttributeComparator()932 private MapComparator<String> getAttributeComparator() { 933 return dtdData == null 934 ? null 935 : dtdData.dtdType == DtdType.ldml 936 ? CLDRFile.getAttributeOrdering() 937 : dtdData.getAttributeComparator(); 938 } 939 940 /** 941 * Determines if an elementName is contained in the path. 942 * 943 * @param elementName 944 * @return 945 */ contains(String elementName)946 public boolean contains(String elementName) { 947 return findElement(elementName) >= 0; 948 } 949 950 /** add a relative path to this XPathParts. */ addRelative(String path)951 public XPathParts addRelative(String path) { 952 if (frozen) { 953 throw new UnsupportedOperationException("Can't modify frozen Element"); 954 } 955 if (path.startsWith("//")) { 956 elements.clear(); 957 path = path.substring(1); // strip one 958 } else { 959 while (path.startsWith("../")) { 960 path = path.substring(3); 961 trimLast(); 962 } 963 if (!path.startsWith("/")) path = "/" + path; 964 } 965 return addInternal(path, false); 966 } 967 968 /** */ trimLast()969 public XPathParts trimLast() { 970 if (frozen) { 971 throw new UnsupportedOperationException("Can't modify frozen Element"); 972 } 973 makeElementsMutable(); 974 elements.remove(elements.size() - 1); 975 return this; 976 } 977 978 /** 979 * Replace the elements of this XPathParts with clones of the elements of the given other 980 * XPathParts 981 * 982 * @param parts the given other XPathParts (not modified) 983 * @return this XPathParts (modified) 984 * <p>Called by XPathParts.replace and CldrItem.split. 985 */ 986 // If this is restored, it will need to be modified. 987 // public XPathParts set(XPathParts parts) { 988 // if (frozen) { 989 // throw new UnsupportedOperationException("Can't modify frozen Element"); 990 // } 991 // try { 992 // dtdData = parts.dtdData; 993 // elements.clear(); 994 // for (Element element : parts.elements) { 995 // elements.add((Element) element.clone()); 996 // } 997 // return this; 998 // } catch (CloneNotSupportedException e) { 999 // throw (InternalError) new InternalError().initCause(e); 1000 // } 1001 // } 1002 1003 /** 1004 * Replace up to i with parts 1005 * 1006 * @param i 1007 * @param parts 1008 */ 1009 // If this is restored, it will need to be modified. 1010 // public XPathParts replace(int i, XPathParts parts) { 1011 // if (frozen) { 1012 // throw new UnsupportedOperationException("Can't modify frozen Element"); 1013 // } 1014 // List<Element> temp = elements; 1015 // elements = new ArrayList<>(); 1016 // set(parts); 1017 // for (; i < temp.size(); ++i) { 1018 // elements.add(temp.get(i)); 1019 // } 1020 // return this; 1021 // } 1022 1023 /** 1024 * Utility to write a comment. 1025 * 1026 * @param pw 1027 * @param blockComment TODO 1028 * @param indent 1029 */ writeComment(PrintWriter pw, int indent, String comment, boolean blockComment)1030 static void writeComment(PrintWriter pw, int indent, String comment, boolean blockComment) { 1031 // now write the comment 1032 if (comment.length() == 0) return; 1033 if (blockComment) { 1034 pw.print(Utility.repeat("\t", indent)); 1035 } else { 1036 pw.print(" "); 1037 } 1038 pw.print("<!--"); 1039 if (comment.indexOf(NEWLINE) > 0) { 1040 boolean first = true; 1041 int countEmptyLines = 0; 1042 // trim the line iff the indent != 0. 1043 for (Iterator<String> it = 1044 CldrUtility.splitList(comment, NEWLINE, indent != 0, null).iterator(); 1045 it.hasNext(); ) { 1046 String line = it.next(); 1047 if (line.length() == 0) { 1048 ++countEmptyLines; 1049 continue; 1050 } 1051 if (countEmptyLines != 0) { 1052 for (int i = 0; i < countEmptyLines; ++i) pw.println(); 1053 countEmptyLines = 0; 1054 } 1055 if (first) { 1056 first = false; 1057 line = line.trim(); 1058 pw.print(" "); 1059 } else if (indent != 0) { 1060 pw.print(Utility.repeat("\t", (indent + 1))); 1061 pw.print(" "); 1062 } 1063 pw.println(line); 1064 } 1065 pw.print(Utility.repeat("\t", indent)); 1066 } else { 1067 pw.print(" "); 1068 pw.print(comment.trim()); 1069 pw.print(" "); 1070 } 1071 pw.print("-->"); 1072 if (blockComment) { 1073 pw.println(); 1074 } 1075 } 1076 1077 /** 1078 * Utility to determine if this a language locale? Note: a script is included with the language, 1079 * if there is one. 1080 * 1081 * @param in 1082 * @return 1083 */ isLanguage(String in)1084 public static boolean isLanguage(String in) { 1085 int pos = in.indexOf('_'); 1086 if (pos < 0) return true; 1087 if (in.indexOf('_', pos + 1) >= 0) return false; // no more than 2 subtags 1088 if (in.length() != pos + 5) return false; // second must be 4 in length 1089 return true; 1090 } 1091 1092 /** 1093 * Returns -1 if parent isn't really a parent, 0 if they are identical, and 1 if parent is a 1094 * proper parent 1095 */ isSubLocale(String parent, String possibleSublocale)1096 public static int isSubLocale(String parent, String possibleSublocale) { 1097 if (parent.equals("root")) { 1098 if (parent.equals(possibleSublocale)) return 0; 1099 return 1; 1100 } 1101 if (parent.length() > possibleSublocale.length()) return -1; 1102 if (!possibleSublocale.startsWith(parent)) return -1; 1103 if (parent.length() == possibleSublocale.length()) return 0; 1104 if (possibleSublocale.charAt(parent.length()) != '_') return -1; // last subtag too long 1105 return 1; 1106 } 1107 1108 /** Sets an attribute/value on the first matching element. */ setAttribute( String elementName, String attributeName, String attributeValue)1109 public XPathParts setAttribute( 1110 String elementName, String attributeName, String attributeValue) { 1111 int index = findElement(elementName); 1112 putAttributeValue(index, attributeName, attributeValue); 1113 return this; 1114 } 1115 removeProposed()1116 public XPathParts removeProposed() { 1117 for (int i = 0; i < elements.size(); ++i) { 1118 Element element = elements.get(i); 1119 if (element.getAttributeCount() == 0) { 1120 continue; 1121 } 1122 for (Entry<String, String> attributesAndValues : element.getAttributes().entrySet()) { 1123 String attribute = attributesAndValues.getKey(); 1124 if (!attribute.equals("alt")) { 1125 continue; 1126 } 1127 String attributeValue = attributesAndValues.getValue(); 1128 int pos = attributeValue.indexOf("proposed"); 1129 if (pos < 0) break; 1130 if (pos > 0 && attributeValue.charAt(pos - 1) == '-') 1131 --pos; // backup for "...-proposed" 1132 if (pos == 0) { 1133 putAttributeValue(i, attribute, null); 1134 break; 1135 } 1136 attributeValue = attributeValue.substring(0, pos); // strip it off 1137 putAttributeValue(i, attribute, attributeValue); 1138 break; // there is only one alt! 1139 } 1140 } 1141 return this; 1142 } 1143 setElement(int elementIndex, String newElement)1144 public XPathParts setElement(int elementIndex, String newElement) { 1145 makeElementsMutable(); 1146 if (elementIndex < 0) { 1147 elementIndex += size(); 1148 } 1149 Element element = elements.get(elementIndex); 1150 elements.set(elementIndex, new Element(element, newElement)); 1151 return this; 1152 } 1153 removeElement(int elementIndex)1154 public XPathParts removeElement(int elementIndex) { 1155 makeElementsMutable(); 1156 elements.remove(elementIndex >= 0 ? elementIndex : elementIndex + size()); 1157 return this; 1158 } 1159 findFirstAttributeValue(String attribute)1160 public String findFirstAttributeValue(String attribute) { 1161 for (int i = 0; i < elements.size(); ++i) { 1162 String value = getAttributeValue(i, attribute); 1163 if (value != null) { 1164 return value; 1165 } 1166 } 1167 return null; 1168 } 1169 setAttribute(int elementIndex, String attributeName, String attributeValue)1170 public XPathParts setAttribute(int elementIndex, String attributeName, String attributeValue) { 1171 putAttributeValue(elementIndex, attributeName, attributeValue); 1172 return this; 1173 } 1174 1175 @Override isFrozen()1176 public boolean isFrozen() { 1177 return frozen; 1178 } 1179 1180 @Override freeze()1181 public XPathParts freeze() { 1182 if (!frozen) { 1183 // ensure that it can't be modified. Later we can fix all the call sites to check 1184 // frozen. 1185 List<Element> temp = new ArrayList<>(elements.size()); 1186 for (Element element : elements) { 1187 temp.add(element.makeImmutable()); 1188 } 1189 elements = ImmutableList.copyOf(temp); 1190 frozen = true; 1191 } 1192 return this; 1193 } 1194 1195 @Override cloneAsThawed()1196 public XPathParts cloneAsThawed() { 1197 XPathParts xppClone = new XPathParts(); 1198 /* 1199 * Remember to copy dtdData. 1200 * Reference: https://unicode.org/cldr/trac/ticket/12007 1201 */ 1202 xppClone.dtdData = this.dtdData; 1203 if (!frozen) { 1204 for (Element e : this.elements) { 1205 xppClone.elements.add(e.cloneAsThawed()); 1206 } 1207 } else { 1208 xppClone.elements = this.elements; 1209 } 1210 return xppClone; 1211 } 1212 getFrozenInstance(String path)1213 public static XPathParts getFrozenInstance(String path) { 1214 XPathParts result = cache.get(path); 1215 if (result == null) { 1216 // CLDR-17504: This can recursively create new paths during creation so MUST NOT 1217 // happen inside the lambda of computeIfAbsent(), but freezing the path is safe. 1218 XPathParts unfrozen = new XPathParts().addInternal(path, true); 1219 result = cache.computeIfAbsent(path, (String p) -> unfrozen.freeze()); 1220 } 1221 return result; 1222 } 1223 getDtdData()1224 public DtdData getDtdData() { 1225 return dtdData; 1226 } 1227 getElements()1228 public Set<String> getElements() { 1229 Builder<String> builder = ImmutableSet.builder(); 1230 for (int i = 0; i < elements.size(); ++i) { 1231 builder.add(elements.get(i).getElement()); 1232 } 1233 return builder.build(); 1234 } 1235 getSpecialNondistinguishingAttributes()1236 public Map<String, String> getSpecialNondistinguishingAttributes() { 1237 // This returns the collection of non-distinguishing attribute values that 1238 // *should* appear with blue background in the Survey Tool left column 1239 // (e.g. the numbers attribute for some date patterns). Non-distinguishing 1240 // attributes that should *not* appear must be explicitly listed as 1241 // exclusions here (and distinguishing attributes are all excluded here). 1242 Map<String, String> ueMap = null; // common case, none found. 1243 for (int i = 0; i < this.size(); i++) { 1244 // taken from XPathTable.getUndistinguishingElementsFor, with some cleanup 1245 // from XPathTable.getUndistinguishingElements, we include alt, draft 1246 for (Entry<String, String> entry : this.getAttributes(i).entrySet()) { 1247 String k = entry.getKey(); 1248 if (getDtdData().isDistinguishing(getElement(i), k) 1249 || k.equals( 1250 "alt") // is always distinguishing, so we don't really need this. 1251 || k.equals("draft") 1252 || k.startsWith("xml:")) { 1253 continue; 1254 } 1255 if (ueMap == null) { 1256 ueMap = new TreeMap<>(); 1257 } 1258 ueMap.put(k, entry.getValue()); 1259 } 1260 } 1261 return ueMap; 1262 } 1263 getPathWithoutAlt(String xpath)1264 public static String getPathWithoutAlt(String xpath) { 1265 XPathParts xpp = getFrozenInstance(xpath).cloneAsThawed(); 1266 xpp.removeAttribute("alt"); 1267 return xpp.toString(); 1268 } 1269 removeAttribute(String attribute)1270 private XPathParts removeAttribute(String attribute) { 1271 for (int i = 0; i < elements.size(); ++i) { 1272 putAttributeValue(i, attribute, null); 1273 } 1274 return this; 1275 } 1276 } 1277