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