1 package org.unicode.cldr.util;
2 
3 import java.util.Collections;
4 import java.util.HashMap;
5 import java.util.HashSet;
6 import java.util.Iterator;
7 import java.util.Map;
8 import java.util.Set;
9 import java.util.regex.Pattern;
10 
11 import org.unicode.cldr.util.XPathParts.Comments;
12 
13 import com.ibm.icu.impl.Relation;
14 import com.ibm.icu.text.Normalizer2;
15 import com.ibm.icu.text.UnicodeSet;
16 import com.ibm.icu.util.VersionInfo;
17 
18 public class SimpleXMLSource extends XMLSource {
19     private Map<String, String> xpath_value = CldrUtility.newConcurrentHashMap();
20     private Map<String, String> xpath_fullXPath = CldrUtility.newConcurrentHashMap();
21     private Comments xpath_comments = new Comments(); // map from paths to comments.
22     private Relation<String, String> VALUE_TO_PATH = null;
23     private Object VALUE_TO_PATH_MUTEX = new Object();
24     private VersionInfo dtdVersionInfo;
25 
SimpleXMLSource(String localeID)26     public SimpleXMLSource(String localeID) {
27         this.setLocaleID(localeID);
28     }
29 
30     /**
31      * Create a shallow, locked copy of another XMLSource.
32      *
33      * @param copyAsLockedFrom
34      */
SimpleXMLSource(SimpleXMLSource copyAsLockedFrom)35     protected SimpleXMLSource(SimpleXMLSource copyAsLockedFrom) {
36         this.xpath_value = copyAsLockedFrom.xpath_value;
37         this.xpath_fullXPath = copyAsLockedFrom.xpath_fullXPath;
38         this.xpath_comments = copyAsLockedFrom.xpath_comments;
39         this.setLocaleID(copyAsLockedFrom.getLocaleID());
40         locked = true;
41     }
42 
43     @Override
getValueAtDPath(String xpath)44     public String getValueAtDPath(String xpath) {
45         return xpath_value.get(xpath);
46     }
47 
getValueAtDPathSkippingInheritanceMarker(String xpath)48     public String getValueAtDPathSkippingInheritanceMarker(String xpath) {
49         String result = xpath_value.get(xpath);
50         return CldrUtility.INHERITANCE_MARKER.equals(result) ? null : result;
51     }
52 
53     @Override
getFullPathAtDPath(String xpath)54     public String getFullPathAtDPath(String xpath) {
55         String result = xpath_fullXPath.get(xpath);
56         if (result != null) return result;
57         if (xpath_value.get(xpath) != null) return xpath; // we don't store duplicates
58         // System.err.println("WARNING: "+getLocaleID()+": path not present in data: " + xpath);
59         // return xpath;
60         return null; // throw new IllegalArgumentException("Path not present in data: " + xpath);
61     }
62 
63     @Override
getXpathComments()64     public Comments getXpathComments() {
65         return xpath_comments;
66     }
67 
68     @Override
setXpathComments(Comments xpath_comments)69     public void setXpathComments(Comments xpath_comments) {
70         this.xpath_comments = xpath_comments;
71     }
72 
73     // public void putPathValue(String xpath, String value) {
74     // if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
75     // String distinguishingXPath = CLDRFile.getDistinguishingXPath(xpath, fixedPath);
76     // xpath_value.put(distinguishingXPath, value);
77     // if (!fixedPath[0].equals(distinguishingXPath)) {
78     // xpath_fullXPath.put(distinguishingXPath, fixedPath[0]);
79     // }
80     // }
81     @Override
removeValueAtDPath(String distinguishingXPath)82     public void removeValueAtDPath(String distinguishingXPath) {
83         String oldValue = xpath_value.get(distinguishingXPath);
84         xpath_value.remove(distinguishingXPath);
85         xpath_fullXPath.remove(distinguishingXPath);
86         updateValuePathMapping(distinguishingXPath, oldValue, null);
87     }
88 
89     @Override
iterator()90     public Iterator<String> iterator() { // must be unmodifiable or locked
91         return Collections.unmodifiableSet(xpath_value.keySet()).iterator();
92     }
93 
94     @Override
freeze()95     public XMLSource freeze() {
96         locked = true;
97         return this;
98     }
99 
100     @Override
cloneAsThawed()101     public XMLSource cloneAsThawed() {
102         SimpleXMLSource result = (SimpleXMLSource) super.cloneAsThawed();
103         result.xpath_comments = (Comments) result.xpath_comments.clone();
104         result.xpath_fullXPath = CldrUtility.newConcurrentHashMap(result.xpath_fullXPath);
105         result.xpath_value = CldrUtility.newConcurrentHashMap(result.xpath_value);
106         return result;
107     }
108 
109     @Override
putFullPathAtDPath(String distinguishingXPath, String fullxpath)110     public void putFullPathAtDPath(String distinguishingXPath, String fullxpath) {
111         xpath_fullXPath.put(distinguishingXPath, fullxpath);
112     }
113 
114     @Override
putValueAtDPath(String distinguishingXPath, String value)115     public void putValueAtDPath(String distinguishingXPath, String value) {
116         String oldValue = xpath_value.get(distinguishingXPath);
117         xpath_value.put(distinguishingXPath, value);
118         updateValuePathMapping(distinguishingXPath, oldValue, value);
119     }
120 
updateValuePathMapping(String distinguishingXPath, String oldValue, String newValue)121     private void updateValuePathMapping(String distinguishingXPath, String oldValue, String newValue) {
122         synchronized (VALUE_TO_PATH_MUTEX) {
123             if (VALUE_TO_PATH != null) {
124                 if (oldValue != null) {
125                     VALUE_TO_PATH.remove(normalize(oldValue), distinguishingXPath);
126                 }
127                 if (newValue != null) {
128                     VALUE_TO_PATH.put(normalize(newValue), distinguishingXPath);
129                 }
130             }
131         }
132     }
133 
134     @Override
getPathsWithValue(String valueToMatch, String pathPrefix, Set<String> result)135     public void getPathsWithValue(String valueToMatch, String pathPrefix, Set<String> result) {
136         // build a Relation mapping value to paths, if needed
137         synchronized (VALUE_TO_PATH_MUTEX) {
138             if (VALUE_TO_PATH == null) {
139                 VALUE_TO_PATH = Relation.of(new HashMap<String, Set<String>>(), HashSet.class);
140                 for (Iterator<String> it = iterator(); it.hasNext();) {
141                     String path = it.next();
142                     String value1 = getValueAtDPathSkippingInheritanceMarker(path);
143                     if (value1 == null) {
144                         continue;
145                     }
146                     String value = normalize(value1);
147                     VALUE_TO_PATH.put(value, path);
148                 }
149             }
150             Set<String> paths = VALUE_TO_PATH.getAll(normalize(valueToMatch));
151             if (paths == null) {
152                 return;
153             }
154             if (pathPrefix == null || pathPrefix.length() == 0) {
155                 result.addAll(paths);
156                 return;
157             }
158             for (String path : paths) {
159                 if (path.startsWith(pathPrefix)) {
160                     // if (altPath.originalPath.startsWith(altPrefix.originalPath)) {
161                     result.add(path);
162                 }
163             }
164         }
165     }
166 
167     static final Normalizer2 NFKCCF = Normalizer2.getNFKCCasefoldInstance();
168     static final Normalizer2 NFKC = Normalizer2.getNFKCInstance();
169 
170     // The following includes letters, marks, numbers, currencies, and *selected* symbols/punctuation
171     static final UnicodeSet NON_ALPHANUM = new UnicodeSet("[^[:L:][:M:][:N:][:Sc:]/+\\-°′″%‰‱٪؉−⍰()⊕☉]").freeze();
172 
normalize(String valueToMatch)173     public static String normalize(String valueToMatch) {
174         return replace(NON_ALPHANUM, NFKCCF.normalize(valueToMatch), "");
175     }
176 
normalizeCaseSensitive(String valueToMatch)177     public static String normalizeCaseSensitive(String valueToMatch) {
178         return replace(NON_ALPHANUM, NFKC.normalize(valueToMatch), "");
179     }
180 
replace(UnicodeSet unicodeSet, String valueToMatch, String substitute)181     public static String replace(UnicodeSet unicodeSet, String valueToMatch, String substitute) {
182         // handle patterns
183         if (valueToMatch.contains("{")) {
184             valueToMatch = PLACEHOLDER.matcher(valueToMatch).replaceAll("⍰").trim();
185         }
186         StringBuilder b = null; // delay creating until needed
187         for (int i = 0; i < valueToMatch.length(); ++i) {
188             int cp = valueToMatch.codePointAt(i);
189             if (unicodeSet.contains(cp)) {
190                 if (b == null) {
191                     b = new StringBuilder();
192                     b.append(valueToMatch.substring(0, i)); // copy the start
193                 }
194                 if (substitute.length() != 0) {
195                     b.append(substitute);
196                 }
197             } else if (b != null) {
198                 b.appendCodePoint(cp);
199             }
200             if (cp > 0xFFFF) { // skip end of supplemental character
201                 ++i;
202             }
203         }
204         if (b != null) {
205             valueToMatch = b.toString();
206         }
207         return valueToMatch;
208     }
209 
210     static final Pattern PLACEHOLDER = PatternCache.get("\\{\\d\\}");
211 
setDtdVersionInfo(VersionInfo dtdVersionInfo)212     public void setDtdVersionInfo(VersionInfo dtdVersionInfo) {
213         this.dtdVersionInfo = dtdVersionInfo;
214     }
215 
216     @Override
getDtdVersionInfo()217     public VersionInfo getDtdVersionInfo() {
218         return dtdVersionInfo;
219     }
220 }