• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2009 Google Inc. All Rights Reserved.
2 
3 package org.unicode.cldr.icu;
4 
5 import java.io.File;
6 import java.util.ArrayList;
7 import java.util.HashMap;
8 import java.util.Iterator;
9 import java.util.List;
10 import java.util.Map;
11 
12 import org.unicode.cldr.icu.ICUResourceWriter.Resource;
13 import org.unicode.cldr.icu.ICUResourceWriter.ResourceInt;
14 import org.unicode.cldr.icu.ICUResourceWriter.ResourceString;
15 import org.unicode.cldr.icu.ICUResourceWriter.ResourceTable;
16 
17 public class ResourceSplitter {
18     private final ICULog log;
19     private final List<SplitInfo> splitInfos;
20     private final Map<String, File> targetDirs;
21 
22     public static class SplitInfo {
23         final String srcNodePath;
24         final String targetNodePath;
25         final String targetDirPath;
26 
SplitInfo(String srcNodePath, String targetDirPath)27         public SplitInfo(String srcNodePath, String targetDirPath) {
28             this(srcNodePath, targetDirPath, null);
29         }
30 
SplitInfo(String srcNodePath, String targetDirPath, String targetNodePath)31         public SplitInfo(String srcNodePath, String targetDirPath, String targetNodePath) {
32             // normalize
33             if (!srcNodePath.endsWith("/")) {
34                 srcNodePath += "/";
35             }
36 
37             if (targetNodePath == null) {
38                 targetNodePath = srcNodePath;
39             } else if (!targetNodePath.endsWith("/")) {
40                 targetNodePath += "/";
41             }
42 
43             this.srcNodePath = srcNodePath;
44             this.targetNodePath = targetNodePath;
45             this.targetDirPath = targetDirPath;
46         }
47     }
48 
49     static class ResultInfo {
50         final File directory;
51         final ResourceTable root;
52 
ResultInfo(File directory, ResourceTable root)53         public ResultInfo(File directory, ResourceTable root) {
54             this.directory = directory;
55             this.root = root;
56         }
57     }
58 
59     static class Path {
60         private StringBuilder sb = new StringBuilder();
61         private int[] indices = new int[10]; // default length should be enough
62         private int depth;
63 
Path(String s)64         Path(String s) {
65             sb.append(s);
66         }
67 
fullPath()68         String fullPath() {
69             return sb.toString();
70         }
71 
push(String pathSegment)72         void push(String pathSegment) {
73             if (depth == indices.length) {
74                 int[] temp = new int[depth * 2];
75                 System.arraycopy(indices, 0, temp, 0, depth);
76                 indices = temp;
77             }
78 
79             indices[depth++] = sb.length();
80             sb.append(pathSegment).append("/");
81         }
82 
pop()83         void pop() {
84             if (depth == 0) {
85                 throw new IndexOutOfBoundsException("can't pop past start of path");
86             }
87             sb.setLength(indices[--depth]);
88         }
89     }
90 
ResourceSplitter(ICULog log, String baseDirPath, List<SplitInfo> splitInfos)91     ResourceSplitter(ICULog log, String baseDirPath, List<SplitInfo> splitInfos) {
92         this.log = log;
93         this.splitInfos = splitInfos;
94         this.targetDirs = new HashMap<String, File>();
95 
96         File baseDir = new File(baseDirPath);
97 
98         for (SplitInfo si : splitInfos) {
99             String dirPath = si.targetDirPath;
100             if (!targetDirs.containsKey(dirPath)) {
101                 File dir = new File(dirPath);
102                 if (!dir.isAbsolute()) {
103                     dir = new File(baseDir, dirPath);
104                 }
105                 if (dir.exists()) {
106                     if (!dir.isDirectory()) {
107                         throw new IllegalArgumentException(
108                             "File \"" + dirPath + "\" exists and is not a directory");
109                     }
110                     if (!dir.canWrite()) {
111                         throw new IllegalArgumentException(
112                             "Cannot write to directory \"" + dirPath + "\"");
113                     }
114                 } else {
115                     if (!dir.mkdirs()) {
116                         throw new IllegalArgumentException(
117                             "Unable to create directory path \"" + dirPath + "\"");
118                     }
119                 }
120                 targetDirs.put(dirPath, dir);
121             }
122         }
123     }
124 
split(File targetDir, ResourceTable root)125     public List<ResultInfo> split(File targetDir, ResourceTable root) {
126         return new SplitProcessor(new ResultInfo(targetDir, root)).split();
127     }
128 
129     // Does the actual work of splitting the resource, based on the ResourceSplitter's specs.
130     private class SplitProcessor {
131         private final ResultInfo source;
132 
133         private final Path path;
134         private final Map<String, ResourceTable> resultMap;
135         private final Map<String, ResourceTable> aliasMap;
136         private final List<SplitInfo> remainingInfos;
137 
SplitProcessor(ResultInfo source)138         private SplitProcessor(ResultInfo source) {
139             this.source = source;
140 
141             this.path = new Path("/");
142             this.resultMap = new HashMap<String, ResourceTable>();
143             this.aliasMap = new HashMap<String, ResourceTable>();
144             this.remainingInfos = new ArrayList<SplitInfo>();
145             this.remainingInfos.addAll(splitInfos);
146         }
147 
split()148         private List<ResultInfo> split() {
149             // start split below the root, so we don't match against the locale name
150             if (!handleAlias()) {
151                 process(source.root, source.root.first);
152             }
153 
154             // All trees need a root resource. Add one to any tree that didn't get one.
155             // Not only that, but some locales that use root data rely on the presence of
156             // a resource file matching the prefix of the locale to prevent fallback
157             // lookup through the default locale. To prevent this error, all resources
158             // need at least a language-only stub resource to be present.
159             //
160             // If the locale string does not contain an underscore, we assume that it's
161             // either the 'root' locale or a language-only locale, so we always generate
162             // the resource.
163             //
164             // Arrgh. The icu package tool wants all internal nodes in the tree to be
165             // present. Currently, the missing nodes are all lang_Script locales.
166             // Maybe change the package tool to fix this.
167             int x = source.root.name.indexOf('_');
168             if (x == -1 || source.root.name.length() - x == 5) {
169                 for (String targetDirPath : targetDirs.keySet()) {
170                     ResourceTable root = resultMap.get(targetDirPath);
171                     if (root == null) {
172                         log.log("Generate stub '" + source.root.name + ".txt' in '" + targetDirPath + "'");
173                         getResultRoot(targetDirPath);
174                     }
175                 }
176             }
177 
178             List<ResultInfo> results = new ArrayList<ResultInfo>();
179             results.add(source); // write out what's left of the original
180 
181             for (Map.Entry<String, ResourceTable> e : resultMap.entrySet()) {
182                 File dir = targetDirs.get(e.getKey());
183                 results.add(new ResultInfo(dir, e.getValue()));
184             }
185 
186             for (Map.Entry<String, ResourceTable> e : aliasMap.entrySet()) {
187                 File dir = targetDirs.get(e.getKey());
188                 results.add(new ResultInfo(dir, e.getValue()));
189             }
190 
191             return results;
192         }
193 
194         // if there is an "%%ALIAS" resource at the top level, copy the file to
195         // all target directories. We're done.
196         // Well, maybe not. We need to ensure that all aliases have their targets in
197         // their directories.
198         //
199         // It's probably ok to generate them. if they were created already, we'll
200         // not bother. If we generate one, and there is real data later, we'll
201         // just overwrite it.
handleAlias()202         private boolean handleAlias() {
203             for (Resource res = source.root.first; res != null; res = res.next) {
204                 if ("\"%%ALIAS\"".equals(res.name)) {
205                     // it's an alias, create for all targets
206                     for (String targetDirPath : targetDirs.keySet()) {
207                         log.log("Generate alias '" + source.root.name + "' in '" + targetDirPath + "'");
208                         getResultRoot(targetDirPath);
209                         generateTargetIfNeeded(((ResourceString) res).val, targetDirPath);
210                     }
211                     return true;
212                 }
213             }
214             return false;
215         }
216 
generateTargetIfNeeded(String resName, String dirPath)217         private void generateTargetIfNeeded(String resName, String dirPath) {
218             File targetDir = targetDirs.get(dirPath);
219             String fileName = resName + ".txt";
220             if (!new File(targetDir, fileName).exists()) {
221                 log.log("Generating alias target '" + resName + "' in '" + dirPath + "'");
222 
223                 ResourceTable res = new ResourceTable();
224                 res.name = resName;
225                 ResourceString str = new ResourceString();
226                 str.name = "___";
227                 str.val = "";
228                 str.comment = "empty target resource";
229                 res.first = str;
230 
231                 aliasMap.put(dirPath, res);
232             }
233         }
234 
process(Resource parent, Resource res)235         private void process(Resource parent, Resource res) {
236             while (true) {
237                 Resource next = res.next;
238 
239                 path.push(res.name);
240                 String fullPath = path.fullPath();
241                 for (Iterator<SplitInfo> iter = remainingInfos.iterator(); iter.hasNext();) {
242                     SplitInfo si = iter.next();
243                     if (si.srcNodePath.startsWith(fullPath)) {
244                         if (si.srcNodePath.equals(fullPath)) {
245                             handleSplit(parent, res, si);
246                             iter.remove(); // don't need to look for this path anymore
247                         } else {
248                             if (res.first != null) {
249                                 process(res, res.first);
250                             }
251                         }
252                         break;
253                     }
254                 }
255                 path.pop();
256 
257                 if (next == null) {
258                     break;
259                 }
260 
261                 res = next;
262             }
263         }
264 
handleSplit(Resource parent, Resource res, SplitInfo si)265         private void handleSplit(Resource parent, Resource res, SplitInfo si) {
266             ResourceTable root = getResultRoot(si.targetDirPath);
267 
268             removeChildFromParent(res, parent);
269 
270             placeResourceAtPath(root, si.targetNodePath, res);
271         }
272 
getResultRoot(String targetDirPath)273         private ResourceTable getResultRoot(String targetDirPath) {
274             ResourceTable root = resultMap.get(targetDirPath);
275             if (root == null) {
276                 root = createRoot();
277                 resultMap.put(targetDirPath, root);
278             }
279             return root;
280         }
281 
282         /**
283          * Creates a new ResourceTable root. It is a copy of the top of the source resource.
284          * It includes the Version and %%ParentIsRoot resources from the source resource, if present.
285          */
createRoot()286         private ResourceTable createRoot() {
287             ResourceTable src = source.root;
288             ResourceTable root = new ResourceTable();
289             root.annotation = src.annotation;
290             root.comment = src.comment;
291             root.name = src.name;
292 
293             // if the src contains a version element, copy that element
294             final String versionKey = "Version";
295             final String parentRootKey = "%%ParentIsRoot";
296             final String aliasKey = "\"%%ALIAS\"";
297 
298             for (Resource child = src.first; child != null; child = child.next) {
299                 if (versionKey.equals(child.name)) {
300                     String value = ((ResourceString) child).val;
301                     root.appendContents(ICUResourceWriter.createString(versionKey, value));
302                 } else if (parentRootKey.equals(child.name)) {
303                     ResourceInt parentIsRoot = new ResourceInt();
304                     parentIsRoot.name = parentRootKey;
305                     parentIsRoot.val = ((ResourceInt) child).val;
306                     root.appendContents(parentIsRoot);
307                 } else if (aliasKey.equals(child.name)) {
308                     String value = ((ResourceString) child).val;
309                     root.appendContents(ICUResourceWriter.createString(aliasKey, value));
310                 }
311             }
312 
313             return root;
314         }
315 
316         /**
317          * Ensures that targetNodePath exists rooted at res, and returns the resource at that
318          * path.
319          */
placeResourceAtPath(Resource root, String targetNodePath, Resource res)320         private void placeResourceAtPath(Resource root, String targetNodePath, Resource res) {
321             String[] nodeNames = targetNodePath.split("/");
322 
323             // rename the resource with the last name in the path, and shorten the path
324             int len = nodeNames.length;
325             res.name = nodeNames[--len];
326 
327             // find or build nodes corresponding to remaining path
328             // Skip initial empty node name (because of leading slash in target path)
329             for (int i = 1; i < len; ++i) {
330                 root = findOrCreateNode(root, nodeNames[i]);
331             }
332 
333             // put the renamed node at the end of the new parent
334             root.appendContents(res);
335         }
336 
findOrCreateNode(Resource parent, String nodeName)337         private Resource findOrCreateNode(Resource parent, String nodeName) {
338             // if no children, just create one, set it as the first child, and return it
339             if (parent.first == null) {
340                 ResourceTable newNode = new ResourceTable();
341                 newNode.name = nodeName;
342                 parent.first = newNode;
343                 return newNode;
344             }
345 
346             // if the first child is the one we want, return it
347             if (nodeName.equals(parent.first.name)) {
348                 return parent.first;
349             }
350 
351             // search for the node we want, remembering its 'elder' sibling, and if we find the
352             // one we want, return it
353             Resource child = parent.first;
354             for (; child.next != null; child = child.next) {
355                 if (nodeName.equals(child.next.name)) {
356                     return child.next;
357                 }
358             }
359 
360             // didn't find it, so create the node, make it the sibling of the youngest child,
361             // and return it
362             ResourceTable newNode = new ResourceTable();
363             newNode.name = nodeName;
364             child.next = newNode;
365             return newNode;
366         }
367 
368         /**
369          * Removes this single child resource from parent, leaving other children
370          * of parent undisturbed.
371          *
372          * @return the child resource (with no siblings)
373          */
removeChildFromParent(Resource child, Resource parent)374         private Resource removeChildFromParent(Resource child, Resource parent) {
375             Resource first = parent.first;
376             if (first == child) {
377                 parent.first = child.next;
378             } else {
379                 while (first.next != null && first.next != child) {
380                     first = first.next;
381                 }
382                 if (first.next == null) {
383                     throw new IllegalArgumentException("Resource " + child + " is not a child of " +
384                         parent);
385                 }
386                 first.next = child.next;
387             }
388             child.next = null;
389 
390             return child;
391         }
392     }
393 }