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 }