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