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