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