| // Copyright 2009 Google Inc. All Rights Reserved. |
| |
| package org.unicode.cldr.icu; |
| |
| import java.io.File; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.unicode.cldr.icu.ICUResourceWriter.Resource; |
| import org.unicode.cldr.icu.ICUResourceWriter.ResourceInt; |
| import org.unicode.cldr.icu.ICUResourceWriter.ResourceString; |
| import org.unicode.cldr.icu.ICUResourceWriter.ResourceTable; |
| |
| public class ResourceSplitter { |
| private final ICULog log; |
| private final List<SplitInfo> splitInfos; |
| private final Map<String, File> targetDirs; |
| |
| public static class SplitInfo { |
| final String srcNodePath; |
| final String targetNodePath; |
| final String targetDirPath; |
| |
| public SplitInfo(String srcNodePath, String targetDirPath) { |
| this(srcNodePath, targetDirPath, null); |
| } |
| |
| public SplitInfo(String srcNodePath, String targetDirPath, String targetNodePath) { |
| // normalize |
| if (!srcNodePath.endsWith("/")) { |
| srcNodePath += "/"; |
| } |
| |
| if (targetNodePath == null) { |
| targetNodePath = srcNodePath; |
| } else if (!targetNodePath.endsWith("/")) { |
| targetNodePath += "/"; |
| } |
| |
| this.srcNodePath = srcNodePath; |
| this.targetNodePath = targetNodePath; |
| this.targetDirPath = targetDirPath; |
| } |
| } |
| |
| static class ResultInfo { |
| final File directory; |
| final ResourceTable root; |
| |
| public ResultInfo(File directory, ResourceTable root) { |
| this.directory = directory; |
| this.root = root; |
| } |
| } |
| |
| static class Path { |
| private StringBuilder sb = new StringBuilder(); |
| private int[] indices = new int[10]; // default length should be enough |
| private int depth; |
| |
| Path(String s) { |
| sb.append(s); |
| } |
| |
| String fullPath() { |
| return sb.toString(); |
| } |
| |
| void push(String pathSegment) { |
| if (depth == indices.length) { |
| int[] temp = new int[depth * 2]; |
| System.arraycopy(indices, 0, temp, 0, depth); |
| indices = temp; |
| } |
| |
| indices[depth++] = sb.length(); |
| sb.append(pathSegment).append("/"); |
| } |
| |
| void pop() { |
| if (depth == 0) { |
| throw new IndexOutOfBoundsException("can't pop past start of path"); |
| } |
| sb.setLength(indices[--depth]); |
| } |
| } |
| |
| ResourceSplitter(ICULog log, String baseDirPath, List<SplitInfo> splitInfos) { |
| this.log = log; |
| this.splitInfos = splitInfos; |
| this.targetDirs = new HashMap<String, File>(); |
| |
| File baseDir = new File(baseDirPath); |
| |
| for (SplitInfo si : splitInfos) { |
| String dirPath = si.targetDirPath; |
| if (!targetDirs.containsKey(dirPath)) { |
| File dir = new File(dirPath); |
| if (!dir.isAbsolute()) { |
| dir = new File(baseDir, dirPath); |
| } |
| if (dir.exists()) { |
| if (!dir.isDirectory()) { |
| throw new IllegalArgumentException( |
| "File \"" + dirPath + "\" exists and is not a directory"); |
| } |
| if (!dir.canWrite()) { |
| throw new IllegalArgumentException( |
| "Cannot write to directory \"" + dirPath + "\""); |
| } |
| } else { |
| if (!dir.mkdirs()) { |
| throw new IllegalArgumentException( |
| "Unable to create directory path \"" + dirPath + "\""); |
| } |
| } |
| targetDirs.put(dirPath, dir); |
| } |
| } |
| } |
| |
| public List<ResultInfo> split(File targetDir, ResourceTable root) { |
| return new SplitProcessor(new ResultInfo(targetDir, root)).split(); |
| } |
| |
| // Does the actual work of splitting the resource, based on the ResourceSplitter's specs. |
| private class SplitProcessor { |
| private final ResultInfo source; |
| |
| private final Path path; |
| private final Map<String, ResourceTable> resultMap; |
| private final Map<String, ResourceTable> aliasMap; |
| private final List<SplitInfo> remainingInfos; |
| |
| private SplitProcessor(ResultInfo source) { |
| this.source = source; |
| |
| this.path = new Path("/"); |
| this.resultMap = new HashMap<String, ResourceTable>(); |
| this.aliasMap = new HashMap<String, ResourceTable>(); |
| this.remainingInfos = new ArrayList<SplitInfo>(); |
| this.remainingInfos.addAll(splitInfos); |
| } |
| |
| private List<ResultInfo> split() { |
| // start split below the root, so we don't match against the locale name |
| if (!handleAlias()) { |
| process(source.root, source.root.first); |
| } |
| |
| // All trees need a root resource. Add one to any tree that didn't get one. |
| // Not only that, but some locales that use root data rely on the presence of |
| // a resource file matching the prefix of the locale to prevent fallback |
| // lookup through the default locale. To prevent this error, all resources |
| // need at least a language-only stub resource to be present. |
| // |
| // If the locale string does not contain an underscore, we assume that it's |
| // either the 'root' locale or a language-only locale, so we always generate |
| // the resource. |
| // |
| // Arrgh. The icu package tool wants all internal nodes in the tree to be |
| // present. Currently, the missing nodes are all lang_Script locales. |
| // Maybe change the package tool to fix this. |
| int x = source.root.name.indexOf('_'); |
| if (x == -1 || source.root.name.length() - x == 5) { |
| for (String targetDirPath : targetDirs.keySet()) { |
| ResourceTable root = resultMap.get(targetDirPath); |
| if (root == null) { |
| log.log("Generate stub '" + source.root.name + ".txt' in '" + targetDirPath + "'"); |
| getResultRoot(targetDirPath); |
| } |
| } |
| } |
| |
| List<ResultInfo> results = new ArrayList<ResultInfo>(); |
| results.add(source); // write out what's left of the original |
| |
| for (Map.Entry<String, ResourceTable> e : resultMap.entrySet()) { |
| File dir = targetDirs.get(e.getKey()); |
| results.add(new ResultInfo(dir, e.getValue())); |
| } |
| |
| for (Map.Entry<String, ResourceTable> e : aliasMap.entrySet()) { |
| File dir = targetDirs.get(e.getKey()); |
| results.add(new ResultInfo(dir, e.getValue())); |
| } |
| |
| return results; |
| } |
| |
| // if there is an "%%ALIAS" resource at the top level, copy the file to |
| // all target directories. We're done. |
| // Well, maybe not. We need to ensure that all aliases have their targets in |
| // their directories. |
| // |
| // It's probably ok to generate them. if they were created already, we'll |
| // not bother. If we generate one, and there is real data later, we'll |
| // just overwrite it. |
| private boolean handleAlias() { |
| for (Resource res = source.root.first; res != null; res = res.next) { |
| if ("\"%%ALIAS\"".equals(res.name)) { |
| // it's an alias, create for all targets |
| for (String targetDirPath : targetDirs.keySet()) { |
| log.log("Generate alias '" + source.root.name + "' in '" + targetDirPath + "'"); |
| getResultRoot(targetDirPath); |
| generateTargetIfNeeded(((ResourceString) res).val, targetDirPath); |
| } |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private void generateTargetIfNeeded(String resName, String dirPath) { |
| File targetDir = targetDirs.get(dirPath); |
| String fileName = resName + ".txt"; |
| if (!new File(targetDir, fileName).exists()) { |
| log.log("Generating alias target '" + resName + "' in '" + dirPath + "'"); |
| |
| ResourceTable res = new ResourceTable(); |
| res.name = resName; |
| ResourceString str = new ResourceString(); |
| str.name = "___"; |
| str.val = ""; |
| str.comment = "empty target resource"; |
| res.first = str; |
| |
| aliasMap.put(dirPath, res); |
| } |
| } |
| |
| private void process(Resource parent, Resource res) { |
| while (true) { |
| Resource next = res.next; |
| |
| path.push(res.name); |
| String fullPath = path.fullPath(); |
| for (Iterator<SplitInfo> iter = remainingInfos.iterator(); iter.hasNext();) { |
| SplitInfo si = iter.next(); |
| if (si.srcNodePath.startsWith(fullPath)) { |
| if (si.srcNodePath.equals(fullPath)) { |
| handleSplit(parent, res, si); |
| iter.remove(); // don't need to look for this path anymore |
| } else { |
| if (res.first != null) { |
| process(res, res.first); |
| } |
| } |
| break; |
| } |
| } |
| path.pop(); |
| |
| if (next == null) { |
| break; |
| } |
| |
| res = next; |
| } |
| } |
| |
| private void handleSplit(Resource parent, Resource res, SplitInfo si) { |
| ResourceTable root = getResultRoot(si.targetDirPath); |
| |
| removeChildFromParent(res, parent); |
| |
| placeResourceAtPath(root, si.targetNodePath, res); |
| } |
| |
| private ResourceTable getResultRoot(String targetDirPath) { |
| ResourceTable root = resultMap.get(targetDirPath); |
| if (root == null) { |
| root = createRoot(); |
| resultMap.put(targetDirPath, root); |
| } |
| return root; |
| } |
| |
| /** |
| * Creates a new ResourceTable root. It is a copy of the top of the source resource. |
| * It includes the Version and %%ParentIsRoot resources from the source resource, if present. |
| */ |
| private ResourceTable createRoot() { |
| ResourceTable src = source.root; |
| ResourceTable root = new ResourceTable(); |
| root.annotation = src.annotation; |
| root.comment = src.comment; |
| root.name = src.name; |
| |
| // if the src contains a version element, copy that element |
| final String versionKey = "Version"; |
| final String parentRootKey = "%%ParentIsRoot"; |
| final String aliasKey = "\"%%ALIAS\""; |
| |
| for (Resource child = src.first; child != null; child = child.next) { |
| if (versionKey.equals(child.name)) { |
| String value = ((ResourceString) child).val; |
| root.appendContents(ICUResourceWriter.createString(versionKey, value)); |
| } else if (parentRootKey.equals(child.name)) { |
| ResourceInt parentIsRoot = new ResourceInt(); |
| parentIsRoot.name = parentRootKey; |
| parentIsRoot.val = ((ResourceInt) child).val; |
| root.appendContents(parentIsRoot); |
| } else if (aliasKey.equals(child.name)) { |
| String value = ((ResourceString) child).val; |
| root.appendContents(ICUResourceWriter.createString(aliasKey, value)); |
| } |
| } |
| |
| return root; |
| } |
| |
| /** |
| * Ensures that targetNodePath exists rooted at res, and returns the resource at that |
| * path. |
| */ |
| private void placeResourceAtPath(Resource root, String targetNodePath, Resource res) { |
| String[] nodeNames = targetNodePath.split("/"); |
| |
| // rename the resource with the last name in the path, and shorten the path |
| int len = nodeNames.length; |
| res.name = nodeNames[--len]; |
| |
| // find or build nodes corresponding to remaining path |
| // Skip initial empty node name (because of leading slash in target path) |
| for (int i = 1; i < len; ++i) { |
| root = findOrCreateNode(root, nodeNames[i]); |
| } |
| |
| // put the renamed node at the end of the new parent |
| root.appendContents(res); |
| } |
| |
| private Resource findOrCreateNode(Resource parent, String nodeName) { |
| // if no children, just create one, set it as the first child, and return it |
| if (parent.first == null) { |
| ResourceTable newNode = new ResourceTable(); |
| newNode.name = nodeName; |
| parent.first = newNode; |
| return newNode; |
| } |
| |
| // if the first child is the one we want, return it |
| if (nodeName.equals(parent.first.name)) { |
| return parent.first; |
| } |
| |
| // search for the node we want, remembering its 'elder' sibling, and if we find the |
| // one we want, return it |
| Resource child = parent.first; |
| for (; child.next != null; child = child.next) { |
| if (nodeName.equals(child.next.name)) { |
| return child.next; |
| } |
| } |
| |
| // didn't find it, so create the node, make it the sibling of the youngest child, |
| // and return it |
| ResourceTable newNode = new ResourceTable(); |
| newNode.name = nodeName; |
| child.next = newNode; |
| return newNode; |
| } |
| |
| /** |
| * Removes this single child resource from parent, leaving other children |
| * of parent undisturbed. |
| * |
| * @return the child resource (with no siblings) |
| */ |
| private Resource removeChildFromParent(Resource child, Resource parent) { |
| Resource first = parent.first; |
| if (first == child) { |
| parent.first = child.next; |
| } else { |
| while (first.next != null && first.next != child) { |
| first = first.next; |
| } |
| if (first.next == null) { |
| throw new IllegalArgumentException("Resource " + child + " is not a child of " + |
| parent); |
| } |
| first.next = child.next; |
| } |
| child.next = null; |
| |
| return child; |
| } |
| } |
| } |