xref: /aosp_15_r20/external/cldr/tools/cldr-code/src/main/java/org/unicode/cldr/util/XPathParts.java (revision 912701f9769bb47905792267661f0baf2b85bed5)
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 public final class XPathParts extends XPathParser
39         implements Freezable<XPathParts>, Comparable<XPathParts> {
40     private static final boolean DEBUGGING = false;
41 
42     private volatile boolean frozen = false;
43 
44     private List<Element> elements = new ArrayList<>();
45 
46     private DtdData dtdData = null;
47 
48     private static final Map<String, XPathParts> cache = new ConcurrentHashMap<>();
49 
50     /**
51      * Construct a new empty XPathParts object.
52      *
53      * <p>Note: for faster performance, call getFrozenInstance or getInstance instead of this
54      * constructor. This constructor remains public for special cases in which individual elements
55      * are added with addElement rather than using a complete path string.
56      */
XPathParts()57     public XPathParts() {}
58 
59     /** See if the xpath contains an element */
containsElement(String element)60     public boolean containsElement(String element) {
61         for (int i = 0; i < elements.size(); ++i) {
62             if (elements.get(i).getElement().equals(element)) {
63                 return true;
64             }
65         }
66         return false;
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? */
containsAttribute(String attribute)365     public boolean containsAttribute(String attribute) {
366         for (int i = 0; i < elements.size(); ++i) {
367             Element element = elements.get(i);
368             if (element.getAttributeValue(attribute) != null) {
369                 return true;
370             }
371         }
372         return false;
373     }
374 
375     /** Does it contain the attribute/value pair? */
containsAttributeValue(String attribute, String value)376     public boolean containsAttributeValue(String attribute, String value) {
377         for (int i = 0; i < elements.size(); ++i) {
378             String otherValue = elements.get(i).getAttributeValue(attribute);
379             if (otherValue != null && value.equals(otherValue)) return true;
380         }
381         return false;
382     }
383 
384     /** How many elements are in this xpath? */
size()385     public int size() {
386         return elements.size();
387     }
388 
389     /** Get the nth element. Negative values are from end */
390     @Override
getElement(int elementIndex)391     public String getElement(int elementIndex) {
392         return elements.get(elementIndex >= 0 ? elementIndex : elementIndex + size()).getElement();
393     }
394 
getAttributeCount(int elementIndex)395     public int getAttributeCount(int elementIndex) {
396         return elements.get(elementIndex >= 0 ? elementIndex : elementIndex + size())
397                 .getAttributeCount();
398     }
399 
400     /**
401      * Get the attributes for the nth element (negative index is from end). Returns null or an empty
402      * map if there's nothing. PROBLEM: exposes internal map
403      */
getAttributes(int elementIndex)404     public Map<String, String> getAttributes(int elementIndex) {
405         return elements.get(elementIndex >= 0 ? elementIndex : elementIndex + size())
406                 .getAttributes();
407     }
408 
409     /**
410      * return non-modifiable collection
411      *
412      * @param elementIndex
413      * @return
414      */
getAttributeKeys(int elementIndex)415     public Collection<String> getAttributeKeys(int elementIndex) {
416         return elements.get(elementIndex >= 0 ? elementIndex : elementIndex + size())
417                 .getAttributes()
418                 .keySet();
419     }
420 
421     /**
422      * Get the attributeValue for the attrbute at the nth element (negative index is from end).
423      * Returns null if there's nothing.
424      */
425     @Override
getAttributeValue(int elementIndex, String attribute)426     public String getAttributeValue(int elementIndex, String attribute) {
427         if (elementIndex < 0) {
428             elementIndex += size();
429         }
430         return elements.get(elementIndex).getAttributeValue(attribute);
431     }
432 
putAttributeValue(int elementIndex, String attribute, String value)433     public void putAttributeValue(int elementIndex, String attribute, String value) {
434         elementIndex = elementIndex >= 0 ? elementIndex : elementIndex + size();
435         Map<String, String> ea = elements.get(elementIndex).attributes;
436         if (value == null && (ea == null || !ea.containsKey(attribute))) {
437             return;
438         }
439         if (value != null && ea != null && value.equals(ea.get(attribute))) {
440             return;
441         }
442         makeElementsMutable();
443         makeElementMutable(elementIndex);
444         // make mutable may change elements.get(elementIndex), so we have to use
445         // elements.get(elementIndex) after calling
446         elements.get(elementIndex).putAttribute(attribute, value);
447     }
448 
449     /**
450      * Get the attributes for the nth element. Returns null or an empty map if there's nothing.
451      * PROBLEM: exposes internal map
452      */
findAttributes(String elementName)453     public Map<String, String> findAttributes(String elementName) {
454         int index = findElement(elementName);
455         if (index == -1) {
456             return null;
457         }
458         return getAttributes(index);
459     }
460 
461     /** Find the attribute value */
findAttributeValue(String elementName, String attributeName)462     public String findAttributeValue(String elementName, String attributeName) {
463         Map<String, String> attributes = findAttributes(elementName);
464         if (attributes == null) {
465             return null;
466         }
467         return attributes.get(attributeName);
468     }
469 
470     @Override
handleClearElements()471     protected void handleClearElements() {
472         elements.clear();
473     }
474 
475     @Override
handleAddElement(String element)476     protected void handleAddElement(String element) {
477         addElement(element);
478     }
479     /**
480      * Add an Element object to this XPathParts, using the given element name. If this is the first
481      * Element in this XPathParts, also set dtdData. Do not set any attributes.
482      *
483      * @param element the string describing the element, such as "ldml", "supplementalData", etc.
484      * @return this XPathParts
485      */
addElement(String element)486     public XPathParts addElement(String element) {
487         if (elements.size() == 0) {
488             try {
489                 /*
490                  * The first element should match one of the DtdType enum values.
491                  * Use it to set dtdData.
492                  */
493                 File dir = CLDRConfig.getInstance().getCldrBaseDirectory();
494                 dtdData = DtdData.getInstance(DtdType.fromElement(element), dir);
495             } catch (Exception e) {
496                 dtdData = null;
497             }
498         }
499         makeElementsMutable();
500         elements.add(new Element(element));
501         return this;
502     }
503 
makeElementsMutable()504     public void makeElementsMutable() {
505         if (frozen) {
506             throw new UnsupportedOperationException("Can't modify frozen object.");
507         }
508 
509         if (elements instanceof ImmutableList) {
510             elements = new ArrayList<>(elements);
511         }
512     }
513 
makeElementMutable(int elementIndex)514     public void makeElementMutable(int elementIndex) {
515         if (frozen) {
516             throw new UnsupportedOperationException("Can't modify frozen object.");
517         }
518 
519         Element e = elements.get(elementIndex);
520         Map<String, String> ea = e.attributes;
521         if (ea == null || ea instanceof ImmutableMap) {
522             elements.set(elementIndex, e.cloneAsThawed());
523         }
524     }
525 
526     /**
527      * Varargs version of addElement. Usage: xpp.addElements("ldml","localeDisplayNames")
528      *
529      * @param element
530      * @return this for chaining
531      */
addElements(String... element)532     public XPathParts addElements(String... element) {
533         for (String e : element) {
534             addElement(e);
535         }
536         return this;
537     }
538 
539     @Override
handleAddAttribute(String attribute, String value)540     protected void handleAddAttribute(String attribute, String value) {
541         addAttribute(attribute, value);
542     }
543 
544     /** Add an attribute/value pair to the current last element. */
addAttribute(String attribute, String value)545     public XPathParts addAttribute(String attribute, String value) {
546         putAttributeValue(elements.size() - 1, attribute, value);
547         return this;
548     }
549 
removeAttribute(String elementName, String attributeName)550     public XPathParts removeAttribute(String elementName, String attributeName) {
551         return removeAttribute(findElement(elementName), attributeName);
552     }
553 
removeAttribute(int elementIndex, String attributeName)554     public XPathParts removeAttribute(int elementIndex, String attributeName) {
555         putAttributeValue(elementIndex, attributeName, null);
556         return this;
557     }
558 
removeAttributes(String elementName, Collection<String> attributeNames)559     public XPathParts removeAttributes(String elementName, Collection<String> attributeNames) {
560         return removeAttributes(findElement(elementName), attributeNames);
561     }
562 
removeAttributes(int elementIndex, Collection<String> attributeNames)563     public XPathParts removeAttributes(int elementIndex, Collection<String> attributeNames) {
564         elementIndex = elementIndex >= 0 ? elementIndex : elementIndex + size();
565         Map<String, String> ea = elements.get(elementIndex).attributes;
566         if (ea == null
567                 || attributeNames == null
568                 || attributeNames.isEmpty()
569                 || Collections.disjoint(attributeNames, ea.keySet())) {
570             return this;
571         }
572         makeElementsMutable();
573         makeElementMutable(elementIndex);
574         // make mutable may change elements.get(elementIndex), so we have to use
575         // elements.get(elementIndex) after calling
576         elements.get(elementIndex).removeAttributes(attributeNames);
577         return this;
578     }
579 
580     /**
581      * Add the given path to this XPathParts.
582      *
583      * @param xPath the path string
584      * @param initial boolean, if true, call elements.clear() and set dtdData = null before adding,
585      *     and make requiredPrefix // instead of /
586      * @return the XPathParts, or parseError
587      *     <p>Called by set (initial = true), and addRelative (initial = false)
588      */
addInternal(String xPath, boolean initial)589     private XPathParts addInternal(String xPath, boolean initial) {
590         if (initial) {
591             dtdData = null;
592         }
593 
594         // call superclass for parsing
595         handleParse(xPath, initial);
596 
597         return this;
598     }
599 
600     /** boilerplate */
601     @Override
toString()602     public String toString() {
603         return toString(elements.size());
604     }
605 
606     // TODO combine and optimize these
607 
toString(int limit)608     public String toString(int limit) {
609         if (limit < 0) {
610             limit += size();
611         }
612         String result = "/";
613         try {
614             for (int i = 0; i < limit; ++i) {
615                 result += elements.get(i).toString(XPATH_STYLE);
616             }
617         } catch (RuntimeException e) {
618             throw e;
619         }
620         return result;
621     }
622 
toString(int start, int limit)623     public String toString(int start, int limit) {
624         if (start < 0) {
625             start += size();
626         }
627         if (limit < 0) {
628             limit += size();
629         }
630         StringBuilder result = new StringBuilder();
631         for (int i = start; i < limit; ++i) {
632             result.append(elements.get(i).toString(XPATH_STYLE));
633         }
634         return result.toString();
635     }
636 
637     /** boilerplate */
638     @Override
equals(Object other)639     public boolean equals(Object other) {
640         try {
641             XPathParts that = (XPathParts) other;
642             if (elements.size() != that.elements.size()) return false;
643             for (int i = 0; i < elements.size(); ++i) {
644                 if (!elements.get(i).equals(that.elements.get(i))) {
645                     return false;
646                 }
647             }
648             return true;
649         } catch (ClassCastException e) {
650             return false;
651         }
652     }
653 
654     @Override
compareTo(XPathParts that)655     public int compareTo(XPathParts that) {
656         return dtdData.getDtdComparator().xpathComparator(this, that);
657     }
658 
659     /** boilerplate */
660     @Override
hashCode()661     public int hashCode() {
662         int result = elements.size();
663         for (int i = 0; i < elements.size(); ++i) {
664             result = result * 37 + elements.get(i).hashCode();
665         }
666         return result;
667     }
668 
669     // ========== Privates ==========
670 
671     public static final int XPATH_STYLE = 0, XML_OPEN = 1, XML_CLOSE = 2, XML_NO_VALUE = 3;
672     public static final String NEWLINE = "\n";
673 
674     private final class Element {
675         private final String element;
676         private Map<String, String> attributes; // = new TreeMap(AttributeComparator);
677 
Element(String element)678         public Element(String element) {
679             this(element, null);
680         }
681 
Element(Element other, String element)682         public Element(Element other, String element) {
683             this(element, other.attributes);
684         }
685 
Element(String element, Map<String, String> attributes)686         public Element(String element, Map<String, String> attributes) {
687             this.element = element.intern(); // allow fast comparison
688             if (attributes == null) {
689                 this.attributes = null;
690             } else {
691                 this.attributes = new TreeMap<>(getAttributeComparator());
692                 this.attributes.putAll(attributes);
693             }
694         }
695 
696         /**
697          * Add the given attribute, value pair to this Element object; or, if value is null, remove
698          * the attribute.
699          *
700          * @param attribute, the string such as "number" or "cldrVersion"
701          * @param value, the string such as "$Revision$" or "35", or null for removal
702          */
putAttribute(String attribute, String value)703         public void putAttribute(String attribute, String value) {
704             attribute = attribute.intern(); // allow fast comparison
705             if (value == null) {
706                 if (attributes != null) {
707                     attributes.remove(attribute);
708                     if (attributes.size() == 0) {
709                         attributes = null;
710                     }
711                 }
712             } else {
713                 if (attributes == null) {
714                     attributes = new TreeMap<>(getAttributeComparator());
715                 }
716                 attributes.put(attribute, value);
717             }
718         }
719 
720         /**
721          * Remove the given attributes from this Element object.
722          *
723          * @param attributeNames
724          */
removeAttributes(Collection<String> attributeNames)725         private void removeAttributes(Collection<String> attributeNames) {
726             if (attributeNames == null) {
727                 return;
728             }
729             for (String attribute : attributeNames) {
730                 attributes.remove(attribute);
731             }
732             if (attributes.size() == 0) {
733                 attributes = null;
734             }
735         }
736 
737         @Override
toString()738         public String toString() {
739             throw new IllegalArgumentException("Don't use");
740         }
741 
742         /**
743          * @param style from XPATH_STYLE
744          * @return
745          */
toString(int style)746         public String toString(int style) {
747             StringBuilder result = new StringBuilder();
748             // Set keys;
749             switch (style) {
750                 case XPathParts.XPATH_STYLE:
751                     result.append('/').append(element);
752                     writeAttributes("[@", "\"]", false, result);
753                     break;
754                 case XPathParts.XML_OPEN:
755                 case XPathParts.XML_NO_VALUE:
756                     result.append('<').append(element);
757                     writeAttributes(" ", "\"", true, result);
758                     if (style == XML_NO_VALUE) {
759                         result.append('/');
760                     }
761                     if (CLDRFile.HACK_ORDER && element.equals("ldml")) {
762                         result.append(' ');
763                     }
764                     result.append('>');
765                     break;
766                 case XML_CLOSE:
767                     result.append("</").append(element).append('>');
768                     break;
769             }
770             return result.toString();
771         }
772 
773         /**
774          * @param element TODO
775          * @param prefix TODO
776          * @param postfix TODO
777          * @param removeLDMLExtras TODO
778          * @param result
779          */
writeAttributes( String prefix, String postfix, boolean removeLDMLExtras, StringBuilder result)780         private Element writeAttributes(
781                 String prefix, String postfix, boolean removeLDMLExtras, StringBuilder result) {
782             if (getAttributeCount() == 0) {
783                 return this;
784             }
785             Map<String, Map<String, String>> suppressionMap = null;
786             if (removeLDMLExtras) {
787                 suppressionMap = CLDRFile.getDefaultSuppressionMap();
788             }
789             for (Entry<String, String> attributesAndValues : attributes.entrySet()) {
790                 String attribute = attributesAndValues.getKey();
791                 String value = attributesAndValues.getValue();
792                 if (removeLDMLExtras && suppressionMap != null) {
793                     if (skipAttribute(element, attribute, value, suppressionMap)) {
794                         continue;
795                     }
796                     if (skipAttribute("*", attribute, value, suppressionMap)) {
797                         continue;
798                     }
799                 }
800                 try {
801                     result.append(prefix)
802                             .append(attribute)
803                             .append("=\"")
804                             .append(
805                                     removeLDMLExtras
806                                             ? TransliteratorUtilities.toHTML.transliterate(value)
807                                             : value)
808                             .append(postfix);
809                 } catch (RuntimeException e) {
810                     throw e; // for debugging
811                 }
812             }
813             return this;
814         }
815 
816         /**
817          * Should writeAttributes skip the given element, attribute, and value?
818          *
819          * @param element
820          * @param attribute
821          * @param value
822          * @return true to skip, else false
823          *     <p>Called only by writeAttributes
824          *     <p>Assume suppressionMap isn't null.
825          */
skipAttribute( String element, String attribute, String value, Map<String, Map<String, String>> suppressionMap)826         private boolean skipAttribute(
827                 String element,
828                 String attribute,
829                 String value,
830                 Map<String, Map<String, String>> suppressionMap) {
831             Map<String, String> attribute_value = suppressionMap.get(element);
832             boolean skip = false;
833             if (attribute_value != null) {
834                 Object suppressValue = attribute_value.get(attribute);
835                 if (suppressValue == null) {
836                     suppressValue = attribute_value.get("*");
837                 }
838                 if (suppressValue != null) {
839                     if (value.equals(suppressValue) || suppressValue.equals("*")) {
840                         skip = true;
841                     }
842                 }
843             }
844             return skip;
845         }
846 
847         @Override
equals(Object other)848         public boolean equals(Object other) {
849             if (other == null) {
850                 return false;
851             }
852             try {
853                 Element that = (Element) other;
854                 // == check is ok since we intern elements
855                 return element == that.element
856                         && (attributes == null
857                                 ? that.attributes == null
858                                 : that.attributes == null
859                                         ? attributes == null
860                                         : attributes.equals(that.attributes));
861             } catch (ClassCastException e) {
862                 return false;
863             }
864         }
865 
866         @Override
hashCode()867         public int hashCode() {
868             return element.hashCode() * 37 + (attributes == null ? 0 : attributes.hashCode());
869         }
870 
getElement()871         public String getElement() {
872             return element;
873         }
874 
getAttributeCount()875         private int getAttributeCount() {
876             if (attributes == null) {
877                 return 0;
878             }
879             return attributes.size();
880         }
881 
getAttributes()882         private Map<String, String> getAttributes() {
883             if (attributes == null) {
884                 return ImmutableMap.of();
885             }
886             return ImmutableMap.copyOf(attributes);
887         }
888 
getAttributeValue(String attribute)889         private String getAttributeValue(String attribute) {
890             if (attributes == null) {
891                 return null;
892             }
893             return attributes.get(attribute);
894         }
895 
makeImmutable()896         public Element makeImmutable() {
897             if (attributes != null && !(attributes instanceof ImmutableMap)) {
898                 attributes = ImmutableMap.copyOf(attributes);
899             }
900 
901             return this;
902         }
903 
cloneAsThawed()904         public Element cloneAsThawed() {
905             return new Element(element, attributes);
906         }
907     }
908 
909     /**
910      * Search for an element within the path.
911      *
912      * @param elementName the element to look for
913      * @return element number if found, else -1 if not found
914      */
findElement(String elementName)915     public int findElement(String elementName) {
916         for (int i = 0; i < elements.size(); ++i) {
917             Element e = elements.get(i);
918             if (!e.getElement().equals(elementName)) {
919                 continue;
920             }
921             return i;
922         }
923         return -1;
924     }
925 
926     /**
927      * Get the MapComparator for this XPathParts.
928      *
929      * @return the MapComparator, or null
930      *     <p>Called by the Element constructor, and by putAttribute
931      */
getAttributeComparator()932     private MapComparator<String> getAttributeComparator() {
933         return dtdData == null
934                 ? null
935                 : dtdData.dtdType == DtdType.ldml
936                         ? CLDRFile.getAttributeOrdering()
937                         : dtdData.getAttributeComparator();
938     }
939 
940     /**
941      * Determines if an elementName is contained in the path.
942      *
943      * @param elementName
944      * @return
945      */
contains(String elementName)946     public boolean contains(String elementName) {
947         return findElement(elementName) >= 0;
948     }
949 
950     /** add a relative path to this XPathParts. */
addRelative(String path)951     public XPathParts addRelative(String path) {
952         if (frozen) {
953             throw new UnsupportedOperationException("Can't modify frozen Element");
954         }
955         if (path.startsWith("//")) {
956             elements.clear();
957             path = path.substring(1); // strip one
958         } else {
959             while (path.startsWith("../")) {
960                 path = path.substring(3);
961                 trimLast();
962             }
963             if (!path.startsWith("/")) path = "/" + path;
964         }
965         return addInternal(path, false);
966     }
967 
968     /** */
trimLast()969     public XPathParts trimLast() {
970         if (frozen) {
971             throw new UnsupportedOperationException("Can't modify frozen Element");
972         }
973         makeElementsMutable();
974         elements.remove(elements.size() - 1);
975         return this;
976     }
977 
978     /**
979      * Replace the elements of this XPathParts with clones of the elements of the given other
980      * XPathParts
981      *
982      * @param parts the given other XPathParts (not modified)
983      * @return this XPathParts (modified)
984      *     <p>Called by XPathParts.replace and CldrItem.split.
985      */
986     //   If this is restored, it will need to be modified.
987     //    public XPathParts set(XPathParts parts) {
988     //        if (frozen) {
989     //            throw new UnsupportedOperationException("Can't modify frozen Element");
990     //        }
991     //        try {
992     //            dtdData = parts.dtdData;
993     //            elements.clear();
994     //            for (Element element : parts.elements) {
995     //                elements.add((Element) element.clone());
996     //            }
997     //            return this;
998     //        } catch (CloneNotSupportedException e) {
999     //            throw (InternalError) new InternalError().initCause(e);
1000     //        }
1001     //    }
1002 
1003     /**
1004      * Replace up to i with parts
1005      *
1006      * @param i
1007      * @param parts
1008      */
1009     //    If this is restored, it will need to be modified.
1010     //    public XPathParts replace(int i, XPathParts parts) {
1011     //        if (frozen) {
1012     //            throw new UnsupportedOperationException("Can't modify frozen Element");
1013     //        }
1014     //        List<Element> temp = elements;
1015     //        elements = new ArrayList<>();
1016     //        set(parts);
1017     //        for (; i < temp.size(); ++i) {
1018     //            elements.add(temp.get(i));
1019     //        }
1020     //        return this;
1021     //    }
1022 
1023     /**
1024      * Utility to write a comment.
1025      *
1026      * @param pw
1027      * @param blockComment TODO
1028      * @param indent
1029      */
writeComment(PrintWriter pw, int indent, String comment, boolean blockComment)1030     static void writeComment(PrintWriter pw, int indent, String comment, boolean blockComment) {
1031         // now write the comment
1032         if (comment.length() == 0) return;
1033         if (blockComment) {
1034             pw.print(Utility.repeat("\t", indent));
1035         } else {
1036             pw.print(" ");
1037         }
1038         pw.print("<!--");
1039         if (comment.indexOf(NEWLINE) > 0) {
1040             boolean first = true;
1041             int countEmptyLines = 0;
1042             // trim the line iff the indent != 0.
1043             for (Iterator<String> it =
1044                             CldrUtility.splitList(comment, NEWLINE, indent != 0, null).iterator();
1045                     it.hasNext(); ) {
1046                 String line = it.next();
1047                 if (line.length() == 0) {
1048                     ++countEmptyLines;
1049                     continue;
1050                 }
1051                 if (countEmptyLines != 0) {
1052                     for (int i = 0; i < countEmptyLines; ++i) pw.println();
1053                     countEmptyLines = 0;
1054                 }
1055                 if (first) {
1056                     first = false;
1057                     line = line.trim();
1058                     pw.print(" ");
1059                 } else if (indent != 0) {
1060                     pw.print(Utility.repeat("\t", (indent + 1)));
1061                     pw.print(" ");
1062                 }
1063                 pw.println(line);
1064             }
1065             pw.print(Utility.repeat("\t", indent));
1066         } else {
1067             pw.print(" ");
1068             pw.print(comment.trim());
1069             pw.print(" ");
1070         }
1071         pw.print("-->");
1072         if (blockComment) {
1073             pw.println();
1074         }
1075     }
1076 
1077     /**
1078      * Utility to determine if this a language locale? Note: a script is included with the language,
1079      * if there is one.
1080      *
1081      * @param in
1082      * @return
1083      */
isLanguage(String in)1084     public static boolean isLanguage(String in) {
1085         int pos = in.indexOf('_');
1086         if (pos < 0) return true;
1087         if (in.indexOf('_', pos + 1) >= 0) return false; // no more than 2 subtags
1088         if (in.length() != pos + 5) return false; // second must be 4 in length
1089         return true;
1090     }
1091 
1092     /**
1093      * Returns -1 if parent isn't really a parent, 0 if they are identical, and 1 if parent is a
1094      * proper parent
1095      */
isSubLocale(String parent, String possibleSublocale)1096     public static int isSubLocale(String parent, String possibleSublocale) {
1097         if (parent.equals("root")) {
1098             if (parent.equals(possibleSublocale)) return 0;
1099             return 1;
1100         }
1101         if (parent.length() > possibleSublocale.length()) return -1;
1102         if (!possibleSublocale.startsWith(parent)) return -1;
1103         if (parent.length() == possibleSublocale.length()) return 0;
1104         if (possibleSublocale.charAt(parent.length()) != '_') return -1; // last subtag too long
1105         return 1;
1106     }
1107 
1108     /** Sets an attribute/value on the first matching element. */
setAttribute( String elementName, String attributeName, String attributeValue)1109     public XPathParts setAttribute(
1110             String elementName, String attributeName, String attributeValue) {
1111         int index = findElement(elementName);
1112         putAttributeValue(index, attributeName, attributeValue);
1113         return this;
1114     }
1115 
removeProposed()1116     public XPathParts removeProposed() {
1117         for (int i = 0; i < elements.size(); ++i) {
1118             Element element = elements.get(i);
1119             if (element.getAttributeCount() == 0) {
1120                 continue;
1121             }
1122             for (Entry<String, String> attributesAndValues : element.getAttributes().entrySet()) {
1123                 String attribute = attributesAndValues.getKey();
1124                 if (!attribute.equals("alt")) {
1125                     continue;
1126                 }
1127                 String attributeValue = attributesAndValues.getValue();
1128                 int pos = attributeValue.indexOf("proposed");
1129                 if (pos < 0) break;
1130                 if (pos > 0 && attributeValue.charAt(pos - 1) == '-')
1131                     --pos; // backup for "...-proposed"
1132                 if (pos == 0) {
1133                     putAttributeValue(i, attribute, null);
1134                     break;
1135                 }
1136                 attributeValue = attributeValue.substring(0, pos); // strip it off
1137                 putAttributeValue(i, attribute, attributeValue);
1138                 break; // there is only one alt!
1139             }
1140         }
1141         return this;
1142     }
1143 
setElement(int elementIndex, String newElement)1144     public XPathParts setElement(int elementIndex, String newElement) {
1145         makeElementsMutable();
1146         if (elementIndex < 0) {
1147             elementIndex += size();
1148         }
1149         Element element = elements.get(elementIndex);
1150         elements.set(elementIndex, new Element(element, newElement));
1151         return this;
1152     }
1153 
removeElement(int elementIndex)1154     public XPathParts removeElement(int elementIndex) {
1155         makeElementsMutable();
1156         elements.remove(elementIndex >= 0 ? elementIndex : elementIndex + size());
1157         return this;
1158     }
1159 
findFirstAttributeValue(String attribute)1160     public String findFirstAttributeValue(String attribute) {
1161         for (int i = 0; i < elements.size(); ++i) {
1162             String value = getAttributeValue(i, attribute);
1163             if (value != null) {
1164                 return value;
1165             }
1166         }
1167         return null;
1168     }
1169 
setAttribute(int elementIndex, String attributeName, String attributeValue)1170     public XPathParts setAttribute(int elementIndex, String attributeName, String attributeValue) {
1171         putAttributeValue(elementIndex, attributeName, attributeValue);
1172         return this;
1173     }
1174 
1175     @Override
isFrozen()1176     public boolean isFrozen() {
1177         return frozen;
1178     }
1179 
1180     @Override
freeze()1181     public XPathParts freeze() {
1182         if (!frozen) {
1183             // ensure that it can't be modified. Later we can fix all the call sites to check
1184             // frozen.
1185             List<Element> temp = new ArrayList<>(elements.size());
1186             for (Element element : elements) {
1187                 temp.add(element.makeImmutable());
1188             }
1189             elements = ImmutableList.copyOf(temp);
1190             frozen = true;
1191         }
1192         return this;
1193     }
1194 
1195     @Override
cloneAsThawed()1196     public XPathParts cloneAsThawed() {
1197         XPathParts xppClone = new XPathParts();
1198         /*
1199          * Remember to copy dtdData.
1200          * Reference: https://unicode.org/cldr/trac/ticket/12007
1201          */
1202         xppClone.dtdData = this.dtdData;
1203         if (!frozen) {
1204             for (Element e : this.elements) {
1205                 xppClone.elements.add(e.cloneAsThawed());
1206             }
1207         } else {
1208             xppClone.elements = this.elements;
1209         }
1210         return xppClone;
1211     }
1212 
getFrozenInstance(String path)1213     public static XPathParts getFrozenInstance(String path) {
1214         XPathParts result = cache.get(path);
1215         if (result == null) {
1216             // CLDR-17504: This can recursively create new paths during creation so MUST NOT
1217             // happen inside the lambda of computeIfAbsent(), but freezing the path is safe.
1218             XPathParts unfrozen = new XPathParts().addInternal(path, true);
1219             result = cache.computeIfAbsent(path, (String p) -> unfrozen.freeze());
1220         }
1221         return result;
1222     }
1223 
getDtdData()1224     public DtdData getDtdData() {
1225         return dtdData;
1226     }
1227 
getElements()1228     public Set<String> getElements() {
1229         Builder<String> builder = ImmutableSet.builder();
1230         for (int i = 0; i < elements.size(); ++i) {
1231             builder.add(elements.get(i).getElement());
1232         }
1233         return builder.build();
1234     }
1235 
getSpecialNondistinguishingAttributes()1236     public Map<String, String> getSpecialNondistinguishingAttributes() {
1237         // This returns the collection of non-distinguishing attribute values that
1238         // *should* appear with blue background in the Survey Tool left column
1239         // (e.g. the numbers attribute for some date patterns). Non-distinguishing
1240         // attributes that should *not* appear must be explicitly listed as
1241         // exclusions here (and distinguishing attributes are all excluded here).
1242         Map<String, String> ueMap = null; // common case, none found.
1243         for (int i = 0; i < this.size(); i++) {
1244             // taken from XPathTable.getUndistinguishingElementsFor, with some cleanup
1245             // from XPathTable.getUndistinguishingElements, we include alt, draft
1246             for (Entry<String, String> entry : this.getAttributes(i).entrySet()) {
1247                 String k = entry.getKey();
1248                 if (getDtdData().isDistinguishing(getElement(i), k)
1249                         || k.equals(
1250                                 "alt") // is always distinguishing, so we don't really need this.
1251                         || k.equals("draft")
1252                         || k.startsWith("xml:")) {
1253                     continue;
1254                 }
1255                 if (ueMap == null) {
1256                     ueMap = new TreeMap<>();
1257                 }
1258                 ueMap.put(k, entry.getValue());
1259             }
1260         }
1261         return ueMap;
1262     }
1263 
getPathWithoutAlt(String xpath)1264     public static String getPathWithoutAlt(String xpath) {
1265         XPathParts xpp = getFrozenInstance(xpath).cloneAsThawed();
1266         xpp.removeAttribute("alt");
1267         return xpp.toString();
1268     }
1269 
removeAttribute(String attribute)1270     private XPathParts removeAttribute(String attribute) {
1271         for (int i = 0; i < elements.size(); ++i) {
1272             putAttributeValue(i, attribute, null);
1273         }
1274         return this;
1275     }
1276 }
1277