blob: e5379fe028501c48f5b8925d81c6962e94072c3b [file] [log] [blame]
// 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;
}
}
}