Home | History | Annotate | Download | only in util
      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 
     45     public XPathParts() {
     46         this(null, null, null);
     47     }
     48 
     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 
     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      */
     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      */
     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      */
     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 
    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 
    161         public Comments() {
    162             for (CommentType c : CommentType.values()) {
    163                 comments.put(c, new HashMap<String, String>());
    164             }
    165         }
    166 
    167         public String getComment(CommentType style, String xpath) {
    168             return comments.get(style).get(xpath);
    169         }
    170 
    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 
    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 
    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 
    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          */
    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          */
    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          */
    244         public String getFinalComment() {
    245             return finalComment;
    246         }
    247 
    248         /**
    249          * @param finalComment
    250          *            The finalComment to set.
    251          */
    252         public Comments setFinalComment(String finalComment) {
    253             this.finalComment = finalComment;
    254             return this;
    255         }
    256 
    257         /**
    258          * @return Returns the initialComment.
    259          */
    260         public String getInitialComment() {
    261             return initialComment;
    262         }
    263 
    264         /**
    265          * @param initialComment
    266          *            The initialComment to set.
    267          */
    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          */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    395     public int size() {
    396         return elements.size();
    397     }
    398 
    399     /**
    400      * Get the nth element. Negative values are from end
    401      */
    402     public String getElement(int elementIndex) {
    403         return elements.get(elementIndex >= 0 ? elementIndex : elementIndex + size()).getElement();
    404     }
    405 
    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      */
    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      */
    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      */
    435     public String getAttributeValue(int elementIndex, String attribute) {
    436         if (elementIndex < 0) elementIndex += size();
    437         return elements.get(elementIndex).getAttributeValue(attribute);
    438     }
    439 
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    500     public XPathParts removeAttribute(String elementName, String attributeName) {
    501         return removeAttribute(findElement(elementName), attributeName);
    502     }
    503 
    504     public XPathParts removeAttribute(int elementIndex, String attributeName) {
    505         elements.get(elementIndex >= 0 ? elementIndex : elementIndex + size()).putAttribute(attributeName, null);
    506         return this;
    507     }
    508 
    509     public XPathParts removeAttributes(String elementName, Collection<String> attributeNames) {
    510         return removeAttributes(findElement(elementName), attributeNames);
    511     }
    512 
    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      */
    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      */
    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 
    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      */
    620     public String toString() {
    621         return toString(elements.size());
    622     }
    623 
    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 
    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      */
    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      */
    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 
    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 
    696         public Element(String element) {
    697             this(element, null);
    698         }
    699 
    700         public Element(Element other, String element) {
    701             this(element, other.attributes);
    702         }
    703 
    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
    716         protected Object clone() throws CloneNotSupportedException {
    717             return frozen ? this
    718                 : new Element(element, attributes);
    719         }
    720 
    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 
    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 
    755         public String toString() {
    756             throw new IllegalArgumentException("Don't use");
    757         }
    758 
    759         /**
    760          * @param style
    761          *            from XPATH_STYLE
    762          * @return
    763          */
    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          */
    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 
    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 
    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 
    853         public int hashCode() {
    854             return element.hashCode() * 37 + (attributes == null ? 0 : attributes.hashCode());
    855         }
    856 
    857         public String getElement() {
    858             return element;
    859         }
    860 
    861         // private void setAttributes(Map attributes) {
    862         // this.attributes = attributes;
    863         // }
    864 
    865         private int getAttributeCount() {
    866             if (attributes == null) {
    867                 return 0;
    868             }
    869             return attributes.size();
    870         }
    871 
    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 
    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
    906         public boolean isFrozen() {
    907             return frozen;
    908         }
    909 
    910         @Override
    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
    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      */
    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 
    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      */
    954     public boolean contains(String elementName) {
    955         return findElement(elementName) >= 0;
    956     }
    957 
    958     /**
    959      * add a relative path to this XPathParts.
    960      */
    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      */
    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      */
    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      */
   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      */
   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      */
   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      */
   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      */
   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 
   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 
   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 
   1155     public XPathParts removeElement(int elementIndex) {
   1156         elements.remove(elementIndex >= 0 ? elementIndex : elementIndex + size());
   1157         return this;
   1158     }
   1159 
   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 
   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
   1176     public boolean isFrozen() {
   1177         return frozen;
   1178     }
   1179 
   1180     @Override
   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
   1195     public XPathParts cloneAsThawed() {
   1196         return new XPathParts(elements, null, suppressionMap);
   1197     }
   1198 
   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 
   1207     public static XPathParts getInstance(String path) {
   1208         return getFrozenInstance(path).cloneAsThawed();
   1209     }
   1210 
   1211     public DtdData getDtdData() {
   1212         return dtdData;
   1213     }
   1214 
   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 
   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