• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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