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