/*
 *******************************************************************************
 * Copyright (C) 2004-2012, International Business Machines Corporation and    *
 * others. All Rights Reserved.                                                *
 *******************************************************************************
 *
 * Created on Jul 28, 2004
 *
 */
package org.unicode.cldr.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.unicode.cldr.icu.LDMLConstants;
import org.unicode.cldr.util.XMLFileReader.SimpleHandler;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

/**
 * @author ram
 *
 *         TODO To change the template for this generated type comment go to
 *         Window - Preferences - Java - Code Generation - Code and Comments
 */
public class LDMLUtilities {

    public static final int XML = 0,
        TXT = 1;
    private static final boolean DEBUG = false;

    /**
     * Creates a fully resolved locale starting with root and
     *
     * @param sourceDir
     * @param locale
     * @return
     */
    public static Document getFullyResolvedLDML(String sourceDir, String locale,
        boolean ignoreRoot, boolean ignoreUnavailable,
        boolean ignoreIfNoneAvailable, boolean ignoreDraft) {
        return getFullyResolvedLDML(sourceDir, locale, ignoreRoot, ignoreUnavailable, ignoreIfNoneAvailable,
            ignoreDraft, null);
    }

    private static Document getFullyResolvedLDML(String sourceDir, String locale,
        boolean ignoreRoot, boolean ignoreUnavailable,
        boolean ignoreIfNoneAvailable, boolean ignoreDraft, Map<String, String> stack) {
        Document full = null;
        if (stack != null) {
            // For guarding against cicular references
            String key = "SRC:" + sourceDir + File.separator + locale + ".xml";
            if (stack.get(key) != null) {
                System.err.println("Found circular aliases! " + key);
                System.exit(-1);
            }
            stack.put(key, "");
        }
        // System.err.println("In getFullyResolvedLDML "+sourceDir + " " + locale);
        try {
            full = parse(sourceDir + File.separator + "root.xml", ignoreRoot);
            /*
             * Debugging
             *
             * Node[] list = getNodeArray(full, LDMLConstants.ALIAS);
             * if(list.length>0){
             * System.err.println("Aliases not resolved!. list.getLength() returned "+ list.length);
             * }
             */

            if (DEBUG) {
                try {
                    java.io.OutputStreamWriter writer = new java.io.OutputStreamWriter(
                        new java.io.FileOutputStream("./" + File.separator
                            + "root_debug.xml"), "UTF-8");
                    LDMLUtilities.printDOMTree(full, new PrintWriter(writer),
                        "http://www.unicode.org/cldr/dtd/1.3/ldml.dtd", null);
                    writer.flush();
                } catch (IOException e) {
                    // throw the exceptionaway .. this is for debugging
                }
            }
        } catch (RuntimeException ex) {
            if (!ignoreRoot) {
                throw ex;
            }
        }
        int index = locale.indexOf(".xml");
        if (index > -1) {
            locale = locale.substring(0, index);
        }
        if (locale.equals("root")) {
            full = resolveAliases(full, sourceDir, locale, ignoreDraft, stack);
            return full;
        }
        String[] constituents = locale.split("_");
        String loc = null;
        boolean isAvailable = false;
        // String lastLoc = "root";
        for (int i = 0; i < constituents.length; i++) {
            if (loc == null) {
                loc = constituents[i];
            } else {
                loc = loc + "_" + constituents[i];
            }
            Document doc = null;

            // Try cache
            // doc = readMergeCache(sourceDir, lastLoc, loc);
            // if(doc == null) { ..
            String fileName = sourceDir + File.separator + loc + ".xml";
            File file = new File(fileName);
            if (file.exists()) {
                isAvailable = true;
                doc = parseAndResolveAlias(fileName, loc, ignoreUnavailable);

                /*
                 * Debugging
                 *
                 * Node[] list = getNodeArray(doc, LDMLConstants.ALIAS);
                 * if(list.length>0){
                 * System.err.println("Aliases not resolved!. list.getLength() returned "+ list.length);
                 * }
                 */
                if (full == null) {
                    full = doc;
                } else {
                    StringBuffer xpath = new StringBuffer();
                    mergeLDMLDocuments(full, doc, xpath, loc, sourceDir, ignoreDraft, false);
                    if (DEBUG) {
                        try {
                            java.io.OutputStreamWriter writer = new java.io.OutputStreamWriter(
                                new java.io.FileOutputStream("./" + File.separator + loc
                                    + "_debug.xml"), "UTF-8");
                            LDMLUtilities.printDOMTree(full, new PrintWriter(writer),
                                "http://www.unicode.org/cldr/dtd/1.3/ldml.dtd", null);
                            writer.flush();
                        } catch (IOException e) {
                            // throw the exceptionaway .. this is for debugging
                        }
                    }
                }
                /*
                 * debugging
                 *
                 * Node ec = getNode(full, "//ldml/characters/exemplarCharacters");
                 * if(ec==null){
                 * System.err.println("Could not find exemplarCharacters");
                 * }else{
                 * System.out.println("The chars are: "+ getNodeValue(ec));
                 * }
                 */

                // writeMergeCache(sourceDir, lastLoc, loc, full);
                // lastLoc = loc;
            } else {
                if (!ignoreUnavailable) {
                    throw new RuntimeException("Could not find: " + fileName);
                }
            }
            // TODO: investigate if we really need to revalidate the DOM tree!
            // full = revalidate(full, locale);
        }

        if (ignoreIfNoneAvailable == true && isAvailable == false) {
            return null;
        }

        if (DEBUG) {
            try {
                java.io.OutputStreamWriter writer = new java.io.OutputStreamWriter(
                    new java.io.FileOutputStream("./" + File.separator + locale
                        + "_ba_debug.xml"), "UTF-8");
                LDMLUtilities.printDOMTree(full, new PrintWriter(writer),
                    "http://www.unicode.org/cldr/dtd/1.3/ldml.dtd", null);
                writer.flush();
            } catch (IOException e) {
                // throw the exceptionaway .. this is for debugging
            }
        }
        // get the real locale name
        locale = getLocaleName(full);
        // Resolve the aliases once the data is built
        full = resolveAliases(full, sourceDir, locale, ignoreDraft, stack);

        if (DEBUG) {
            try {
                java.io.OutputStreamWriter writer = new java.io.OutputStreamWriter(
                    new java.io.FileOutputStream("./" + File.separator + locale
                        + "_aa_debug.xml"), "UTF-8");
                LDMLUtilities.printDOMTree(full, new PrintWriter(writer),
                    "http://www.unicode.org/cldr/dtd/1.3/ldml.dtd", null);
                writer.flush();
            } catch (IOException e) {
                // throw the exceptionaway .. this is for debugging
            }
        }
        return full;
    }

    public static String getLocaleName(Document doc) {

        Node ln = LDMLUtilities.getNode(doc, "//ldml/identity/language");
        Node tn = LDMLUtilities.getNode(doc, "//ldml/identity/territory");
        Node sn = LDMLUtilities.getNode(doc, "//ldml/identity/script");
        Node vn = LDMLUtilities.getNode(doc, "//ldml/identity/variant");

        StringBuffer locName = new StringBuffer();
        String lang = LDMLUtilities.getAttributeValue(ln, LDMLConstants.TYPE);
        if (lang != null) {
            locName.append(lang);
        } else {
            throw new IllegalArgumentException("Did not get any value for language node from identity.");
        }
        if (sn != null) {
            String script = LDMLUtilities.getAttributeValue(sn, LDMLConstants.TYPE);
            if (script != null) {
                locName.append("_");
                locName.append(script);
            }
        }
        if (tn != null) {
            String terr = LDMLUtilities.getAttributeValue(tn, LDMLConstants.TYPE);
            if (terr != null) {
                locName.append("_");
                locName.append(terr);
            }
        }
        if (vn != null) {
            String variant = LDMLUtilities.getAttributeValue(vn, LDMLConstants.TYPE);
            if (variant != null && tn != null) {
                locName.append("_");
                locName.append(variant);
            }
        }
        return locName.toString();
    }

    // revalidate wasn't called anywhere.
    // TODO: if needed, reimplement using DOM level 3
    /*
     * public static Document revalidate(Document doc, String fileName){
     * // what a waste!!
     * // to revalidate an in-memory DOM tree we need to first
     * // serialize it to byte array and read it back again.
     * // in DOM level 3 implementation there is API to validate
     * // in-memory DOM trees but the latest implementation of Xerces
     * // can only validate against schemas not DTDs!!!
     * try{
     * // revalidate the document
     * Serializer serializer = SerializerFactory.getSerializer(OutputProperties.getDefaultMethodProperties("xml"));
     * ByteArrayOutputStream os = new ByteArrayOutputStream();
     * serializer.setOutputStream(os);
     * DOMSerializer ds = serializer.asDOMSerializer();
     * //ds.serialize(doc);
     * os.flush();
     * ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
     * doc = parse(new InputSource(is),"Fully resolved: "+fileName, false);
     * return doc;
     * }catch(IOException ex){
     * throw new RuntimeException(ex);
     * }
     * }
     */
    @Deprecated
    public static String convertXPath2ICU(Node alias, Node namespaceNode, StringBuffer fullPath)
        throws TransformerException {
        StringBuilder sb = new StringBuilder(fullPath.toString());
        return convertXPath2ICU(alias, namespaceNode, sb);
    }

    public static String convertXPath2ICU(Node alias, Node namespaceNode, StringBuilder fullPath)
        throws TransformerException {
        Node context = alias.getParentNode();
        StringBuffer icu = new StringBuffer();
        String source = getAttributeValue(alias, LDMLConstants.SOURCE);
        String xpath = getAttributeValue(alias, LDMLConstants.PATH);

        // make sure that the xpaths are valid
        if (namespaceNode == null) {
            XPathAPI_eval(context, fullPath.toString());
            if (xpath != null) {
                XPathAPI_eval(context, xpath);
            }
        } else {
            XPathAPI_eval(context, fullPath.toString(), namespaceNode);
            if (xpath != null) {
                XPathAPI_eval(context, xpath, namespaceNode);
            }
        }
        if (source.equals(LDMLConstants.LOCALE)) {
            icu.append("/");
            icu.append(source.toUpperCase());
        } else {
            icu.append(source);
        }
        if (xpath != null) {
            StringBuilder resolved = XPathTokenizer.relativeToAbsolute(xpath, fullPath);
            // make sure that fullPath is not corrupted!
            XPathAPI_eval(context, fullPath.toString());

            // TODO .. do the conversion
            XPathTokenizer tokenizer = new XPathTokenizer(resolved.toString());

            String token = tokenizer.nextToken();
            while (token != null) {
                if (!token.equals("ldml")) {
                    String equiv = getICUEquivalent(token);
                    if (equiv == null) {
                        throw new IllegalArgumentException("Could not find ICU equivalent for token: " + token);
                    }
                    if (equiv.length() > 0) {
                        icu.append("/");
                        icu.append(equiv);
                    }
                }
                token = tokenizer.nextToken();
            }
        }
        return icu.toString();
    }

    public static String convertXPath2ICU(String source, String xpath, String basePath, String fullPath)
        throws TransformerException {
        // Node context = alias.getParentNode();
        StringBuffer icu = new StringBuffer();

        // TODO: make sure that the xpaths are valid. How?

        if (source.equals(LDMLConstants.LOCALE)) {
            icu.append("/");
            icu.append(source.toUpperCase());
        } else {
            icu.append(source);
        }

        if (xpath != null) {
            StringBuilder fullPathBuffer = new StringBuilder(fullPath);
            StringBuilder resolved = XPathTokenizer.relativeToAbsolute(xpath, fullPathBuffer);
            // TODO: make sure that fullPath is not corrupted! How?
            // XPathAPI.eval(context, fullPath.toString());

            // TODO .. do the conversion
            XPathTokenizer tokenizer = new XPathTokenizer(resolved);

            String token = tokenizer.nextToken();
            while (token != null) {
                if (!token.equals("ldml")) {
                    String equiv = getICUEquivalent(token);
                    if (equiv == null) {
                        throw new IllegalArgumentException("Could not find ICU equivalent for token: " + token);
                    }
                    if (equiv.length() > 0) {
                        icu.append("/");
                        icu.append(equiv);
                    }
                }
                token = tokenizer.nextToken();
            }
        }
        return icu.toString();
    }

    public static String getDayIndexAsString(String type) {
        if (type.equals("sun")) {
            return "0";
        } else if (type.equals("mon")) {
            return "1";
        } else if (type.equals("tue")) {
            return "2";
        } else if (type.equals("wed")) {
            return "3";
        } else if (type.equals("thu")) {
            return "4";
        } else if (type.equals("fri")) {
            return "5";
        } else if (type.equals("sat")) {
            return "6";
        } else {
            throw new IllegalArgumentException("Unknown type: " + type);
        }
    }

    public static String getMonthIndexAsString(String type) {
        return Integer.toString(Integer.parseInt(type) - 1);
    }

    private static String getICUEquivalent(String token) {
        int index = 0;
        if (token.indexOf(LDMLConstants.LDN) > -1) {
            return "";
        } else if (token.indexOf(LDMLConstants.LANGUAGES) > -1) {
            return "Languages";
        } else if (token.indexOf(LDMLConstants.LANGUAGE) > -1) {
            return getAttributeValue(token, LDMLConstants.TYPE);
        } else if (token.indexOf(LDMLConstants.TERRITORIES) > -1) {
            return "Countries";
        } else if (token.indexOf(LDMLConstants.TERRITORY) > -1) {
            return getAttributeValue(token, LDMLConstants.TYPE);
        } else if (token.indexOf(LDMLConstants.SCRIPTS) > -1) {
            return "Scripts";
        } else if (token.indexOf(LDMLConstants.SCRIPT) > -1) {
            return getAttributeValue(token, LDMLConstants.TYPE);
        } else if (token.indexOf(LDMLConstants.VARIANTS) > -1) {
            return "Variants";
        } else if (token.indexOf(LDMLConstants.VARIANT) > -1) {
            return getAttributeValue(token, LDMLConstants.TYPE);
        } else if (token.indexOf(LDMLConstants.KEYS) > -1) {
            return "Keys";
        } else if (token.indexOf(LDMLConstants.KEY) > -1) {
            return getAttributeValue(token, LDMLConstants.TYPE);
        } else if (token.indexOf(LDMLConstants.TYPES) > -1) {
            return "Types";
        } else if ((index = token.indexOf(LDMLConstants.TYPE)) > -1 && token.charAt(index - 1) != '@') {
            String type = getAttributeValue(token, LDMLConstants.TYPE);
            String key = getAttributeValue(token, LDMLConstants.KEY);
            return type + "/" + key;
        } else if (token.indexOf(LDMLConstants.LAYOUT) > -1) {
            return "Layout";
        } else if (token.indexOf(LDMLConstants.ORIENTATION) > -1) {
            // TODO fix this
        } else if (token.indexOf(LDMLConstants.CONTEXT_TRANSFORMS) > -1) {
            return "contextTransforms";
        } else if (token.indexOf(LDMLConstants.CONTEXT_TRANSFORM_USAGE) > -1) {
            return getAttributeValue(token, LDMLConstants.TYPE);
        } else if (token.indexOf(LDMLConstants.CHARACTERS) > -1) {
            return "";
        } else if (token.indexOf(LDMLConstants.EXEMPLAR_CHARACTERS) > -1) {
            return "ExemplarCharacters";
        } else if (token.indexOf(LDMLConstants.MEASUREMENT) > -1) {
            return "";
        } else if (token.indexOf(LDMLConstants.MS) > -1) {
            return "MeasurementSystem";
        } else if (token.indexOf(LDMLConstants.PAPER_SIZE) > -1) {
            return "PaperSize";
        } else if (token.indexOf(LDMLConstants.HEIGHT) > -1) {
            return "0";
        } else if (token.indexOf(LDMLConstants.WIDTH) > -1) {
            return "1";
        } else if (token.indexOf(LDMLConstants.DATES) > -1) {
            return "";
        } else if (token.indexOf(LDMLConstants.LPC) > -1) {
            return "localPatternCharacters";
        } else if (token.indexOf(LDMLConstants.CALENDARS) > -1) {
            return "calendar";
        } else if (token.indexOf(LDMLConstants.DEFAULT) > -1) {
            return "default";
        } else if (token.indexOf(LDMLConstants.CALENDAR) > -1) {
            return getAttributeValue(token, LDMLConstants.TYPE);
        } else if (token.indexOf(LDMLConstants.ERAS) > -1) {
            return "eras";
        } else if (token.indexOf(LDMLConstants.ERAABBR) > -1) {
            return "abbreviated";
        } else if (token.indexOf(LDMLConstants.ERA) > -1) {
            return getAttributeValue(token, LDMLConstants.TYPE);
        } else if (token.indexOf(LDMLConstants.NUMBERS) > -1) {
            // TODO fix this
        } else if (token.indexOf(LDMLConstants.SYMBOLS) > -1) {
            return "NumberElements";
        } else if (token.indexOf(LDMLConstants.DATE_FORMATS) > -1) {
            // TODO fix this
        } else if (token.indexOf(LDMLConstants.DFL) > -1) {
            // TODO fix this
        } else if (token.indexOf(LDMLConstants.DATE_FORMAT) > -1) {
            // TODO fix this
        } else if (token.indexOf(LDMLConstants.TIME_FORMATS) > -1) {
            // TODO fix this
        } else if (token.indexOf(LDMLConstants.TFL) > -1) {
            // TODO fix this
        } else if (token.indexOf(LDMLConstants.TIME_FORMAT) > -1) {
            // TODO fix this
        } else if (token.indexOf(LDMLConstants.DATE_TIME_FORMATS) > -1) {
            // TODO fix this
            return "DateTimePatterns";
        } else if (token.indexOf(LDMLConstants.INTVL_FMTS) > -1) {
            return "intervalFormats";
            // TODO fix this
        } else if (token.indexOf(LDMLConstants.DTFL) > -1) {
            // TODO fix this
        } else if (token.indexOf(LDMLConstants.DATE_TIME_FORMAT) > -1) {
            // TODO fix this
        } else if (token.indexOf(LDMLConstants.INTVL_FMTS) > -1) {
            // TODO fix this
        } else if (token.indexOf(LDMLConstants.CYCLIC_NAME_SETS) > -1) {
            return "cyclicNameSets";
        } else if (token.indexOf(LDMLConstants.CYCLIC_NAME_SET) > -1) {
            return getAttributeValue(token, LDMLConstants.TYPE);
        } else if (token.indexOf(LDMLConstants.CYCLIC_NAME_CONTEXT) > -1) {
            return getAttributeValue(token, LDMLConstants.TYPE);
        } else if (token.indexOf(LDMLConstants.CYCLIC_NAME_WIDTH) > -1) {
            return getAttributeValue(token, LDMLConstants.TYPE);
        } else if (token.indexOf(LDMLConstants.CYCLIC_NAME) > -1) {
            String valStr = getAttributeValue(token, LDMLConstants.TYPE);
            return getMonthIndexAsString(valStr);
        } else if (token.indexOf(LDMLConstants.MONTHS) > -1) {
            return "monthNames";
        } else if (token.indexOf(LDMLConstants.MONTH_PATTERNS) > -1) {
            return "monthPatterns";
        } else if (token.indexOf(LDMLConstants.MONTH_PATTERN_CONTEXT) > -1) {
            return getAttributeValue(token, LDMLConstants.TYPE);
        } else if (token.indexOf(LDMLConstants.MONTH_PATTERN_WIDTH) > -1) {
            return getAttributeValue(token, LDMLConstants.TYPE);
        } else if (token.indexOf(LDMLConstants.MONTH_PATTERN) > -1) {
            return getAttributeValue(token, LDMLConstants.TYPE);
        } else if (token.indexOf(LDMLConstants.MONTH_CONTEXT) > -1) {
            return getAttributeValue(token, LDMLConstants.TYPE);
        } else if (token.indexOf(LDMLConstants.MONTH_WIDTH) > -1) {
            return getAttributeValue(token, LDMLConstants.TYPE);
        } else if (token.indexOf(LDMLConstants.MONTH) > -1) {
            String valStr = getAttributeValue(token, LDMLConstants.TYPE);
            return getMonthIndexAsString(valStr);
        } else if (token.indexOf(LDMLConstants.DAYPERIODS) > -1) {
            return "dayPeriods";
        } else if (token.indexOf(LDMLConstants.DAYS) > -1) {
            return "dayNames";
        } else if (token.indexOf(LDMLConstants.DAY_CONTEXT) > -1) {
            return getAttributeValue(token, LDMLConstants.TYPE);
        } else if (token.indexOf(LDMLConstants.DAY_WIDTH) > -1) {
            return getAttributeValue(token, LDMLConstants.TYPE);
        } else if (token.indexOf(LDMLConstants.DAY) > -1) {
            String dayName = getAttributeValue(token, LDMLConstants.TYPE);
            return getDayIndexAsString(dayName);
        } else if (token.indexOf(LDMLConstants.QUARTER_WIDTH) > -1) {
            return getAttributeValue(token, LDMLConstants.TYPE);
        } else if (token.indexOf(LDMLConstants.QUARTER_CONTEXT) > -1) {
            return getAttributeValue(token, LDMLConstants.TYPE);
        } else if (token.indexOf(LDMLConstants.QUARTERS) > -1) {
            return "quarters";
        } else if (token.indexOf(LDMLConstants.QUARTER) > -1) {
            String valStr = getAttributeValue(token, LDMLConstants.TYPE);
            return getMonthIndexAsString(valStr);
        } else if (token.indexOf(LDMLConstants.COLLATIONS) > -1) {
            return "collations";
        } else if (token.indexOf(LDMLConstants.COLLATION) > -1) {
            return getAttributeValue(token, LDMLConstants.TYPE);
        }

        // TODO: this method is not finished yet
        // the conversion of Xpath to ICU alias path
        // is not as straight forward as I thought
        // need to cater to idiosynchracies of each
        // element node :(
        throw new IllegalArgumentException("Unknown Xpath fragment: " + token);
    }

    /**
     *
     * @param token
     *            XPath token fragment
     * @param attrib
     *            attribute whose value must be fetched
     * @return
     */
    private static String getAttributeValue(String token, String attrib) {
        int attribStart = token.indexOf(attrib);
        int valStart = token.indexOf('=', attribStart) + 1/* skip past the separtor */;
        int valEnd = token.indexOf('@', valStart);
        if (valEnd < 0) {
            valEnd = valStart + (token.length() - valStart - 1);
        } else {
            valEnd = token.length() - 1 /* valEnd should be index */;
        }
        String value = token.substring(valStart, valEnd);
        int s = value.indexOf('\'');
        if (s > -1) {
            s++;
            int e = value.lastIndexOf('\'');
            return value.substring(s, e);
        } else {
            // also handle ""
            s = value.indexOf('"');
            if (s > -1) {
                s++;
                int e = value.lastIndexOf('"');
                return value.substring(s, e);
            }
        }
        return value;
    }

    @Deprecated
    public static Node mergeLDMLDocuments(Document source, Node override, StringBuffer xpath,
        String thisName, String sourceDir, boolean ignoreDraft,
        boolean ignoreVersion) {
        StringBuilder sb = new StringBuilder(xpath.toString());
        return mergeLDMLDocuments(source, override, sb, thisName, sourceDir, ignoreDraft, ignoreVersion);
    }

    /**
     * Resolved Data File
     * <p>
     * To produce fully resolved locale data file from CLDR for a locale ID L, you start with root, and replace/add
     * items from the child locales until you get down to L. More formally, this can be expressed as the following
     * procedure.
     * </p>
     * <ol>
     * <li>Let Result be an empty LDML file.</li>
     *
     * <li>For each Li in the locale chain for L
     * <ol>
     * <li>For each element pair P in the LDML file for Li:
     * <ol>
     * <li>If Result has an element pair Q with an equivalent element chain, remove Q.</li>
     * <li>Add P to Result.</li>
     * </ol>
     * </li>
     * </ol>
     *
     * </li>
     * </ol>
     * <p>
     * Note: when adding an element pair to a result, it has to go in the right order for it to be valid according to
     * the DTD.
     * </p>
     *
     * @param source
     * @param override
     * @return the merged document
     */
    public static Node mergeLDMLDocuments(Document source, Node override, StringBuilder xpath,
        String thisName, String sourceDir, boolean ignoreDraft,
        boolean ignoreVersion) {
        if (source == null) {
            return override;
        }
        if (xpath.length() == 0) {
            xpath.append("/");
        }

        // boolean gotcha = false;
        // String oldx = new String(xpath);
        // if(override.getNodeName().equals("week")) {
        // gotcha = true;
        // System.out.println("SRC: " + getNode(source, xpath.toString()).toString());
        // System.out.println("OVR: " + override.toString());
        // }

        // we know that every child xml file either adds or
        // overrides the elements in parent
        // so we traverse the child, at every node check if
        // if the node is present in the source,
        // if (present)
        // recurse to replace any nodes that need to be overridded
        // else
        // import the node into source
        Node child = override.getFirstChild();
        while (child != null) {
            // we are only concerned with element nodes
            if (child.getNodeType() != Node.ELEMENT_NODE) {
                child = child.getNextSibling();
                continue;
            }
            String childName = child.getNodeName();

            int savedLength = xpath.length();
            xpath.append("/");
            xpath.append(childName);
            appendXPathAttribute(child, xpath, false, false);
            Node nodeInSource = null;

            if (childName.indexOf(":") > -1) {
                nodeInSource = getNode(source, xpath.toString(), child);
            } else {
                nodeInSource = getNode(source, xpath.toString());
            }

            Node parentNodeInSource = null;
            if (nodeInSource == null) {
                // the child xml has a new node
                // that should be added to parent
                String parentXpath = xpath.substring(0, savedLength);

                if (childName.indexOf(":") > -1) {
                    parentNodeInSource = getNode(source, parentXpath, child);
                } else {
                    parentNodeInSource = getNode(source, parentXpath);
                }
                if (parentNodeInSource == null) {
                    throw new RuntimeException("Internal Error");
                }

                Node childToImport = source.importNode(child, true);
                parentNodeInSource.appendChild(childToImport);
            } else if (childName.equals(LDMLConstants.IDENTITY)) {
                if (!ignoreVersion) {
                    // replace the source doc
                    // none of the elements under collations are inherited
                    // only the node as a whole!!
                    parentNodeInSource = nodeInSource.getParentNode();
                    Node childToImport = source.importNode(child, true);
                    parentNodeInSource.replaceChild(childToImport, nodeInSource);
                }
            } else if (childName.equals(LDMLConstants.COLLATION)) {
                // replace the source doc
                // none of the elements under collations are inherited
                // only the node as a whole!!
                parentNodeInSource = nodeInSource.getParentNode();
                Node childToImport = source.importNode(child, true);
                parentNodeInSource.replaceChild(childToImport, nodeInSource);
                // override the validSubLocales attribute
                String val = LDMLUtilities.getAttributeValue(child.getParentNode(), LDMLConstants.VALID_SUBLOCALE);
                NamedNodeMap map = parentNodeInSource.getAttributes();
                Node vs = map.getNamedItem(LDMLConstants.VALID_SUBLOCALE);
                vs.setNodeValue(val);
            } else {
                boolean childElementNodes = areChildrenElementNodes(child);
                boolean sourceElementNodes = areChildrenElementNodes(nodeInSource);
                // System.out.println(childName + ":" + childElementNodes + "/" + sourceElementNodes);
                if (childElementNodes && sourceElementNodes) {
                    // recurse to pickup any children!
                    mergeLDMLDocuments(source, child, xpath, thisName, sourceDir, ignoreDraft, ignoreVersion);
                } else {
                    // we have reached a leaf node now get the
                    // replace to the source doc
                    parentNodeInSource = nodeInSource.getParentNode();
                    Node childToImport = source.importNode(child, true);
                    parentNodeInSource.replaceChild(childToImport, nodeInSource);
                }
            }
            xpath.delete(savedLength, xpath.length());
            child = child.getNextSibling();
        }
        // if(gotcha==true) {
        // System.out.println("Final: " + getNode(source, oldx).toString());
        // }
        return source;
    }

    private static Node[] getNodeArray(Document doc, String tagName) {
        NodeList list = doc.getElementsByTagName(tagName);
        // node list is dynamic .. if a node is deleted, then
        // list is immidiately updated.
        // so first cache the nodes returned and do stuff
        Node[] array = new Node[list.getLength()];
        for (int i = 0; i < list.getLength(); i++) {
            array[i] = list.item(i);
        }
        return array;
    }

    /**
     * Utility to create abosolute Xpath from 1.1 style alias element
     *
     * @param node
     * @param type
     * @return
     */
    public static String getAbsoluteXPath(Node node, String type) {
        StringBuffer xpath = new StringBuffer();
        StringBuffer xpathFragment = new StringBuffer();
        node = node.getParentNode(); // the node is alias node .. get its parent
        if (node == null) {
            throw new IllegalArgumentException("Alias node's parent is null!");
        }
        xpath.append(node.getNodeName());
        if (type != null) {
            xpath.append("[@type='" + type + "']"); // TODO: double quotes?
        }
        Node parent = node;
        while ((parent = parent.getParentNode()) != null) {
            xpathFragment.setLength(0);
            xpathFragment.append(parent.getNodeName());
            if (parent.getNodeType() != Node.DOCUMENT_NODE) {
                appendXPathAttribute(parent, xpathFragment);
                xpath.insert(0, "/");
                xpath.insert(0, xpathFragment);
            }
        }
        xpath.insert(0, "//");
        return xpath.toString();
    }

    /**
     *
     * @param n1
     * @param n2
     *            preferred list
     * @param xpath
     * @return
     */
    private static Node[] mergeNodeLists(Object[] n1, Object[] n2) {
        StringBuffer xp1 = new StringBuffer();
        StringBuffer xp2 = new StringBuffer();
        int l1 = xp1.length(), l2 = xp2.length();
        Map<String, Object> map = new HashMap<String, Object>();
        if (n2 == null || n2.length == 0) {
            Node[] na = new Node[n1.length];
            for (int i = 0; i < n1.length; i++) {
                na[i] = (Node) n1[i];
            }
            return na;
        }
        for (int i = 0; i < n1.length; i++) {
            xp1.append(((Node) n1[i]).getNodeName());
            appendXPathAttribute((Node) n1[i], xp1);
            map.put(xp1.toString(), n1[i]);
            xp1.setLength(l1);
        }
        for (int i = 0; i < n2.length; i++) {
            xp2.append(((Node) n2[i]).getNodeName());
            appendXPathAttribute((Node) n2[i], xp2);
            map.put(xp2.toString(), n2[i]);
            xp2.setLength(l2);
        }
        Object[] arr = map.values().toArray();
        Node[] na = new Node[arr.length];
        for (int i = 0; i < arr.length; i++) {
            na[i] = (Node) arr[i];
        }
        return na;
    }

    /**
     *
     * @param fullyResolvedDoc
     * @param sourceDir
     * @param thisLocale
     */
    // TODO guard against circular aliases
    public static Document resolveAliases(Document fullyResolvedDoc, String sourceDir, String thisLocale,
        boolean ignoreDraft, Map<String, String> stack) {
        Node[] array = getNodeArray(fullyResolvedDoc, LDMLConstants.ALIAS);

        // resolve all the aliases by iterating over
        // the list of nodes
        Node[] replacementList = null;
        Node parent = null;
        String source = null;
        String path = null;
        String type = null;
        for (int i = 0; i < array.length; i++) {
            Node node = array[i];
            /*
             * //stop inherited aliases from overwriting valid locale data
             * //ldml.dtd does not allow alias to have any sibling elements
             * boolean bFoundSibling = false;
             * Node n = node.getNextSibling();
             * while (n != null)
             * {
             * if (n.getNodeType() == Node.ELEMENT_NODE)
             * {
             * // System.err.println ("it's an element node " + n.getNodeName () + "  " + n.getNodeValue());
             * bFoundSibling = true;
             * break;
             * }
             * n = n.getNextSibling();
             * }
             * if (bFoundSibling == true)
             * continue;
             */
            // initialize the stack for every alias!
            stack = new HashMap<String, String>();
            if (node == null) {
                System.err.println("list.item(" + i + ") returned null!. The list reports it's length as: "
                    + array.length);
                continue;
            }
            parent = node.getParentNode();
            // boolean isDraft = isNodeDraft(node);
            source = getAttributeValue(node, LDMLConstants.SOURCE);
            path = getAttributeValue(node, LDMLConstants.PATH);
            type = getAttributeValue(parent, LDMLConstants.TYPE);
            if (parent.getParentNode() == null) {
                // some of the nodes were orphaned by the previous alias resolution .. just continue
                continue;
            }
            if (source != null && path == null) {
                // this LDML 1.1 style alias parse it
                path = getAbsoluteXPath(node, type);
            }
            String key = "SRC:" + thisLocale + ";XPATH:" + getAbsoluteXPath(node, type);
            if (stack.get(key) != null) {
                throw new IllegalStateException("Found circular aliases! " + key);

            }
            stack.put(key, "");
            if (source.equals(LDMLConstants.LOCALE)) {

                Object[] aliasList = getChildNodeListAsArray(getNode(parent, path), false);
                Object[] childList = getChildNodeListAsArray(parent, true);
                replacementList = mergeNodeLists(aliasList, childList);
            } else if (source != null && !source.equals(thisLocale)) {
                // if source is defined then path should not be
                // relative
                if (path.indexOf("..") > 0) {
                    throw new IllegalArgumentException("Cannot parse relative xpath: " + path +
                        " in locale: " + source +
                        " from source locale: " + thisLocale);
                }
                // this is a is an absolute XPath
                Document newDoc = getFullyResolvedLDML(sourceDir, source, false, true, false, ignoreDraft, stack);
                replacementList = getNodeListAsArray(newDoc, path);
            } else {
                // path attribute is referencing another node in this DOM tree
                replacementList = getNodeListAsArray(parent, path);
            }
            if (replacementList != null) {
                parent.removeChild(node);
                int listLen = replacementList.length;
                if (listLen > 1) {
                    // check if the whole locale is aliased
                    // if yes then remove the identity from
                    // the current document!
                    if (path != null && path.equals("//ldml/*")) {
                        Node[] identity = getNodeArray(fullyResolvedDoc, LDMLConstants.IDENTICAL);
                        for (int j = 0; j < identity.length; j++) {
                            parent.removeChild(node);
                        }
                    } else {
                        // remove all the children of the parent node
                        removeChildNodes(parent);
                    }
                    for (int j = 0; j < listLen; j++) {
                        // found an element node in the aliased resource
                        // add to the source
                        Node child = replacementList[j];
                        Node childToImport = fullyResolvedDoc.importNode(child, true);
                        // if(isDraft==true && childToImport.getNodeType() == Node.ELEMENT_NODE){
                        // ((Element)childToImport).setAttribute("draft", "true");
                        // }
                        parent.appendChild(childToImport);
                    }
                } else {
                    Node replacement = replacementList[0];
                    // remove all the children of the parent node
                    removeChildNodes(parent);
                    for (Node child = replacement.getFirstChild(); child != null; child = child.getNextSibling()) {
                        // found an element node in the aliased resource
                        // add to the source
                        Node childToImport = fullyResolvedDoc.importNode(child, true);
                        // if(isDraft==true && childToImport.getNodeType() == Node.ELEMENT_NODE){
                        // ((Element)childToImport).setAttribute("draft", "true");
                        // }
                        parent.appendChild(childToImport);
                    }
                }

            } else {
                throw new IllegalArgumentException("Could not find node for xpath: " + path +
                    " in locale: " + source +
                    " from source locale: " + thisLocale);

            }
        }
        return fullyResolvedDoc;
    }

    private static void removeChildNodes(Node parent) {
        Node[] children = toNodeArray(parent.getChildNodes());
        for (int j = 0; j < children.length; j++) {
            parent.removeChild(children[j]);
        }
    }

    // TODO add funtions for fetching legitimate children
    // for ICU
    public boolean isParentDraft(Document fullyResolved, String xpath) {
        Node node = getNode(fullyResolved, xpath);
        Node parentNode;
        while ((parentNode = node.getParentNode()) != null) {
            String draft = getAttributeValue(parentNode, LDMLConstants.DRAFT);
            if (draft != null) {
                if (draft.equals("true") || draft.equals("provisional") || draft.equals("unconfirmed")) {
                    return true;
                } else {
                    return false;
                }
            }
        }
        // the default value is false if none specified
        return false;
    }

    public static boolean isNodeDraft(Node node) {
        String draft = getAttributeValue(node, LDMLConstants.DRAFT);
        if (draft != null) {
            if (draft.equals("true") || draft.equals("provisional") || draft.equals("unconfirmed")) {
                return true;
            } else {
                return false;
            }
        }
        return false;
    }

    public static boolean isDraft(Node fullyResolved, StringBuffer xpath) {
        Node current = getNode(fullyResolved, xpath.toString());
        String draft = null;
        while (current != null && current.getNodeType() == Node.ELEMENT_NODE) {
            draft = getAttributeValue(current, LDMLConstants.DRAFT);
            if (draft != null) {
                if (draft.equals("true") || draft.equals("provisional") || draft.equals("unconfirmed")) {
                    return true;
                } else {
                    return false;
                }
            }
            current = current.getParentNode();
        }
        return false;
    }

    public static boolean isSiblingDraft(Node root) {
        Node current = root;
        String draft = null;
        while (current != null && current.getNodeType() == Node.ELEMENT_NODE) {
            draft = getAttributeValue(current, LDMLConstants.DRAFT);
            if (draft != null) {
                if (draft.equals("true") || draft.equals("provisional") || draft.equals("unconfirmed")) {
                    return true;
                } else {
                    return false;
                }
            }
            current = current.getNextSibling();
        }
        return false;
    }

    public static void appendAllAttributes(Node node, StringBuffer xpath) {
        NamedNodeMap attr = node.getAttributes();
        int len = attr.getLength();
        if (len > 0) {
            for (int i = 0; i < len; i++) {
                Node item = attr.item(i);
                xpath.append("[@");
                xpath.append(item.getNodeName());
                xpath.append("='");
                xpath.append(item.getNodeValue());
                xpath.append("']");
            }
        }
    }

    private static boolean areChildNodesElements(Node node) {
        NodeList list = node.getChildNodes();
        for (int i = 0; i < list.getLength(); i++) {
            if (list.item(i).getNodeType() == Node.ELEMENT_NODE) {
                return true;
            }
        }
        return false;
    }

    private static boolean areSiblingsOfNodeElements(Node node) {
        NodeList list = node.getParentNode().getChildNodes();
        int count = 0;
        for (int i = 0; i < list.getLength(); i++) {
            Node item = list.item(i);
            if (item.getNodeType() == Node.ELEMENT_NODE) {
                count++;
            }
            // the first child node of type element of <ldml> should be <identity>
            // here we figure out if any additional elements are there
            if (count > 1) {
                return true;
            }
        }
        return false;
    }

    public static boolean isLocaleAlias(Document doc) {
        return (getAliasNode(doc) != null);
    }

    private static Node getAliasNode(Document doc) {
        NodeList elements = doc.getElementsByTagName(LDMLConstants.IDENTITY);
        if (elements.getLength() == 1) {
            Node id = elements.item(0);
            Node sib = id;
            while ((sib = sib.getNextSibling()) != null) {
                if (sib.getNodeType() != Node.ELEMENT_NODE) {
                    continue;
                }
                if (sib.getNodeName().equals(LDMLConstants.ALIAS)) {
                    return sib;
                }
            }
        } else {
            System.out.println("Error: elements returned more than 1 identity element!");
        }
        return null;
    }

    /**
     * Determines if the whole locale is marked draft. To accomplish this
     * the method traverses all leaf nodes to determine if all nodes are marked draft
     */
    private static boolean seenElementsOtherThanIdentity = false;

    public static final boolean isLocaleDraft(Node node) {
        boolean isDraft = true;
        // fast path to check if <ldml> element is draft
        if (isNodeDraft(node) == true) {
            return true;
        }
        for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
            if (child.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }
            String name = child.getNodeName();
            // fast path to check if <ldml> element is draft
            if (name.equals(LDMLConstants.LDML) && isNodeDraft(child) == true) {
                return true;
            }
            if (name.equals(LDMLConstants.IDENTITY)) {
                seenElementsOtherThanIdentity = areSiblingsOfNodeElements(child);
                continue;
            }

            if (child.hasChildNodes() && areChildNodesElements(child)) {
                isDraft = isLocaleDraft(child);
            } else {
                if (isNodeDraft(child) == false) {
                    isDraft = false;
                }
            }
            if (isDraft == false) {
                break;
            }
        }
        if (!seenElementsOtherThanIdentity) {
            return false;
        }
        return isDraft;
    }

    /**
     * Appends the attribute values that make differentiate 2 siblings
     * in LDML
     *
     * @param node
     * @param xpath
     * @deprecated - use version that takes StringBuilder instead
     */
    @Deprecated
    public static final void appendXPathAttribute(Node node, StringBuffer xpath) {
        appendXPathAttribute(node, xpath, false, false);
    }

    /**
     * @deprecated
     */
    @Deprecated
    public static void appendXPathAttribute(Node node, StringBuffer xpath, boolean ignoreAlt, boolean ignoreDraft) {
        StringBuilder sb = new StringBuilder(xpath.toString());
        appendXPathAttribute(node, sb, ignoreAlt, ignoreDraft);
    }

    public static final void appendXPathAttribute(Node node, StringBuilder xpath) {
        appendXPathAttribute(node, xpath, false, false);
    }

    public static void appendXPathAttribute(Node node, StringBuilder xpath, boolean ignoreAlt, boolean ignoreDraft) {
        boolean terminate = false;
        String val = getAttributeValue(node, LDMLConstants.TYPE);
        String and = "][";// " and ";
        boolean isStart = true;
        String name = node.getNodeName();
        if (val != null && !name.equals(LDMLConstants.DEFAULT) && !name.equals(LDMLConstants.MS)) {
            if (!(val.equals("standard") && name.equals(LDMLConstants.PATTERN))) {

                if (isStart) {
                    xpath.append("[");
                    isStart = false;
                }
                xpath.append("@type='");
                xpath.append(val);
                xpath.append("'");
                terminate = true;
            }
        }
        if (!ignoreAlt) {
            val = getAttributeValue(node, LDMLConstants.ALT);
            if (val != null) {
                if (isStart) {
                    xpath.append("[");
                    isStart = false;
                } else {
                    xpath.append(and);
                }
                xpath.append("@alt='");
                xpath.append(val);
                xpath.append("'");
                terminate = true;
            }

        }

        if (!ignoreDraft) {
            val = getAttributeValue(node, LDMLConstants.DRAFT);
            if (val != null && !name.equals(LDMLConstants.LDML)) {
                if (isStart) {
                    xpath.append("[");
                    isStart = false;
                } else {
                    xpath.append(and);
                }
                xpath.append("@draft='");
                xpath.append(val);
                xpath.append("'");
                terminate = true;
            }

        }

        val = getAttributeValue(node, LDMLConstants.KEY);
        if (val != null) {
            if (isStart) {
                xpath.append("[");
                isStart = false;
            } else {
                xpath.append(and);
            }
            xpath.append("@key='");
            xpath.append(val);
            xpath.append("'");
            terminate = true;
        }
        val = getAttributeValue(node, LDMLConstants.REGISTRY);
        if (val != null) {
            if (isStart) {
                xpath.append("[");
                isStart = false;
            } else {
                xpath.append(and);
            }
            xpath.append("@registry='");
            xpath.append(val);
            xpath.append("'");
            terminate = true;
        }
        val = getAttributeValue(node, LDMLConstants.ID);
        if (val != null) {
            if (isStart) {
                xpath.append("[");
                isStart = false;
            } else {
                xpath.append(and);
            }
            xpath.append("@id='");
            xpath.append(val);
            xpath.append("'");
            terminate = true;
        }
        if (terminate) {
            xpath.append("]");
        }
    }

    /**
     * Ascertains if the children of the given node are element
     * nodes.
     *
     * @param node
     * @return
     */
    public static boolean areChildrenElementNodes(Node node) {
        NodeList list = node.getChildNodes();
        for (int i = 0; i < list.getLength(); i++) {
            if (list.item(i).getNodeType() == Node.ELEMENT_NODE) {
                return true;
            }
        }
        return false;
    }

    public static Node[] getNodeListAsArray(Node doc, String xpath) {
        try {
            NodeList list = XPathAPI_selectNodeList(doc, xpath);
            int length = list.getLength();
            if (length > 0) {
                Node[] array = new Node[length];
                for (int i = 0; i < length; i++) {
                    array[i] = list.item(i);
                }
                return array;
            }
            return null;
        } catch (TransformerException ex) {
            throw new RuntimeException(ex);
        }
    }

    private static Object[] getChildNodeListAsArray(Node parent, boolean exceptAlias) {

        NodeList list = parent.getChildNodes();
        int length = list.getLength();

        List<Node> al = new ArrayList<Node>();
        for (int i = 0; i < length; i++) {
            Node item = list.item(i);
            if (item.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }
            if (exceptAlias && item.getNodeName().equals(LDMLConstants.ALIAS)) {
                continue;
            }
            al.add(item);
        }
        return al.toArray();

    }

    public static Node[] toNodeArray(NodeList list) {
        int length = list.getLength();
        if (length > 0) {
            Node[] array = new Node[length];
            for (int i = 0; i < length; i++) {
                array[i] = list.item(i);
            }
            return array;
        }
        return null;
    }

    public static Node[] getElementsByTagName(Document doc, String tagName) {
        try {
            NodeList list = doc.getElementsByTagName(tagName);
            int length = list.getLength();
            if (length > 0) {
                Node[] array = new Node[length];
                for (int i = 0; i < length; i++) {
                    array[i] = list.item(i);
                }
                return array;
            }
            return null;
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Fetches the list of nodes that match the given xpath
     *
     * @param doc
     * @param xpath
     * @return
     */
    public static NodeList getNodeList(Document doc, String xpath) {
        try {
            return XPathAPI_selectNodeList(doc, xpath);

        } catch (TransformerException ex) {
            throw new RuntimeException(ex);
        }
    }

    public static final boolean isAlternate(Node node) {
        NamedNodeMap attributes = node.getAttributes();
        Node attr = attributes.getNamedItem(LDMLConstants.ALT);
        if (attr != null) {
            return true;
        }
        return false;
    }

    private static final Node getNonAltNodeIfPossible(NodeList list)
    {
        // A nonalt node is one which .. does not have alternate
        // attribute set
        Node node = null;
        for (int i = 0; i < list.getLength(); i++)
        {
            node = list.item(i);
            if (/* !isDraft(node, xpath)&& */!isAlternate(node))
            {
                return node;
            }
        }
        if (list.getLength() > 0)
            return list.item(0); // if all have alt=.... then return the first one
        return null;
    }

    public static Node getNonAltNodeLike(Node parent, Node child) {
        StringBuffer childXpath = new StringBuffer(child.getNodeName());
        appendXPathAttribute(child, childXpath, true/* ignore alt */, true/* ignore draft */);
        String childXPathString = childXpath.toString();
        for (Node other = parent.getFirstChild(); other != null; other = other.getNextSibling()) {
            if ((other.getNodeType() != Node.ELEMENT_NODE) || (other == child)) {
                continue;
            }
            StringBuffer otherXpath = new StringBuffer(other.getNodeName());
            appendXPathAttribute(other, otherXpath);
            // System.out.println("Compare: " + childXpath + " to " + otherXpath);
            if (childXPathString.equals(otherXpath.toString())) {
                // System.out.println("Match!");
                return other;
            }
        }
        return null;
    }

    /**
     * Fetches the node from the document that matches the given xpath.
     * The context namespace node is required if the xpath contains
     * namespace elments
     *
     * @param doc
     * @param xpath
     * @param namespaceNode
     * @return
     */
    public static Node getNode(Document doc, String xpath, Node namespaceNode) {
        try {
            NodeList nl = XPathAPI_selectNodeList(doc, xpath, namespaceNode);
            int len = nl.getLength();
            // TODO watch for attribute "alt"
            if (len > 1) {
                throw new IllegalArgumentException("The XPATH returned more than 1 node!. Check XPATH: " + xpath);
            }
            if (len == 0) {
                return null;
            }
            return nl.item(0);

        } catch (TransformerException ex) {
            ex.printStackTrace();
            throw new RuntimeException(ex);
        }
    }

    public static Node getNode(Node context, String resToFetch, Node namespaceNode) {
        try {
            NodeList nl = XPathAPI_selectNodeList(context, "./" + resToFetch, namespaceNode);
            int len = nl.getLength();
            // TODO watch for attribute "alt"
            if (len > 1) {
                throw new IllegalArgumentException("The XPATH returned more than 1 node!. Check XPATH: " + resToFetch);
            }
            if (len == 0) {
                return null;
            }
            return nl.item(0);

        } catch (TransformerException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Fetches the node from the document which matches the xpath
     *
     * @param node
     * @param xpath
     * @return
     */
    public static Node getNode(Node node, String xpath) {
        try {
            NodeList nl = XPathAPI_selectNodeList(node, xpath);
            int len = nl.getLength();
            // TODO watch for attribute "alt"
            if (len > 1) {
                // PN Node best = getNonAltNode(nl);
                Node best = getNonAltNodeIfPossible(nl); // PN
                if (best != null) {
                    // System.err.println("Chose best node from " + xpath);
                    return best;
                }
                /* else complain */
                String all = "";
                int i;
                for (i = 0; i < len; i++) {
                    all = all + ", " + nl.item(i);
                }
                throw new IllegalArgumentException("The XPATH returned more than 1 node!. Check XPATH: " + xpath
                    + " = " + all);
            }
            if (len == 0) {
                return null;
            }
            return nl.item(0);

        } catch (TransformerException ex) {
            throw new RuntimeException(ex);
        }
    }

    public static Node getNode(Node node, String xpath, boolean preferDraft, boolean preferAlt) {
        try {
            NodeList nl = XPathAPI_selectNodeList(node, xpath);
            return getNode(nl, xpath, preferDraft, preferAlt);

        } catch (TransformerException ex) {
            throw new RuntimeException(ex);
        }
    }

    private static Node getVettedNode(NodeList list, StringBuffer xpath, boolean ignoreDraft) {
        // A vetted node is one which is not draft and does not have alternate
        // attribute set
        Node node = null;
        for (int i = 0; i < list.getLength(); i++) {
            node = list.item(i);
            if (isDraft(node, xpath) && !ignoreDraft) {
                continue;
            }
            if (isAlternate(node)) {
                continue;
            }
            return node;
        }
        return null;
    }

    public static Node getVettedNode(Document fullyResolvedDoc, Node parent, String childName, StringBuffer xpath,
        boolean ignoreDraft) {
        NodeList list = getNodeList(parent, childName, fullyResolvedDoc, xpath.toString());
        int oldLength = xpath.length();
        Node ret = null;

        if (list != null && list.getLength() > 0) {
            xpath.append("/");
            xpath.append(childName);
            ret = getVettedNode(list, xpath, ignoreDraft);
        }
        xpath.setLength(oldLength);
        return ret;
    }

    public static Node getNode(NodeList nl, String xpath, boolean preferDraft, boolean preferAlt) {
        int len = nl.getLength();
        // TODO watch for attribute "alt"
        if (len > 1) {
            Node best = null;
            for (int i = 0; i < len; i++) {
                Node current = nl.item(i);
                if (!preferDraft && !preferAlt) {
                    if (!isNodeDraft(current) && !isAlternate(current)) {
                        best = current;
                        break;
                    }
                    continue;
                } else if (preferDraft && !preferAlt) {
                    if (isNodeDraft(current) && !isAlternate(current)) {
                        best = current;
                        break;
                    }
                    continue;
                } else if (!preferDraft && preferAlt) {
                    if (!isNodeDraft(current) && isAlternate(current)) {
                        best = current;
                        break;
                    }
                    continue;
                } else {
                    if (isNodeDraft(current) || isAlternate(current)) {
                        best = current;
                        break;
                    }
                    continue;
                }
            }
            if (best == null && preferDraft == true) {
                best = getVettedNode(nl, new StringBuffer(xpath), false);
            }
            if (best != null) {
                return best;
            }
            /* else complain */
            String all = "";
            int i;
            for (i = 0; i < len; i++) {
                all = all + ", " + nl.item(i);
            }
            throw new IllegalArgumentException("The XPATH returned more than 1 node!. Check XPATH: " + xpath + " = "
                + all);
        }
        if (len == 0) {
            return null;
        }
        return nl.item(0);

    }

    /**
     *
     * @param context
     * @param resToFetch
     * @param fullyResolved
     * @param xpath
     * @return
     */
    public static Node getNode(Node context, String resToFetch, Document fullyResolved, String xpath) {
        String ctx = "./" + resToFetch;
        Node node = getNode(context, ctx);
        if (node == null && fullyResolved != null) {
            // try from fully resolved
            String path = xpath + "/" + resToFetch;
            node = getNode(fullyResolved, path);
        }
        return node;
    }

    /**
     *
     * @param context
     * @param resToFetch
     * @return
     */
    public static NodeList getChildNodes(Node context, String resToFetch) {
        String ctx = "./" + resToFetch;
        NodeList list = getNodeList(context, ctx);
        return list;
    }

    /**
     * Fetches the node from the document that matches the given xpath.
     * The context namespace node is required if the xpath contains
     * namespace elments
     *
     * @param doc
     * @param xpath
     * @param namespaceNode
     * @return
     */
    public static NodeList getNodeList(Document doc, String xpath, Node namespaceNode) {
        try {
            NodeList nl = XPathAPI_selectNodeList(doc, xpath, namespaceNode);
            if (nl.getLength() == 0) {
                return null;
            }
            return nl;

        } catch (TransformerException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Fetches the node from the document which matches the xpath
     *
     * @param node
     * @param xpath
     * @return
     */
    public static NodeList getNodeList(Node node, String xpath) {
        try {
            NodeList nl = XPathAPI_selectNodeList(node, xpath);
            int len = nl.getLength();
            if (len == 0) {
                return null;
            }
            return nl;
        } catch (TransformerException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Fetches node list from the children of the context node.
     *
     * @param context
     * @param resToFetch
     * @param fullyResolved
     * @param xpath
     * @return
     */
    public static NodeList getNodeList(Node context, String resToFetch, Document fullyResolved, String xpath) {
        String ctx = "./" + resToFetch;
        NodeList list = getNodeList(context, ctx);
        if ((list == null || list.getLength() > 0) && fullyResolved != null) {
            // try from fully resolved
            String path = xpath + "/" + resToFetch;
            list = getNodeList(fullyResolved, path);
        }
        return list;
    }

    /**
     * Decide if the node is text, and so must be handled specially
     *
     * @param n
     * @return
     */
    public static Node getAttributeNode(Node sNode, String attribName) {
        NamedNodeMap attrs = sNode.getAttributes();
        if (attrs != null) {
            return attrs.getNamedItem(attribName);
        }
        return null;
    }

    /**
     * Utility method to fetch the attribute value from the given
     * element node
     *
     * @param sNode
     * @param attribName
     * @return
     */
    public static String getAttributeValue(Node sNode, String attribName) {
        String value = null;
        NamedNodeMap attrs = sNode.getAttributes();
        if (attrs != null) {
            Node attr = attrs.getNamedItem(attribName);
            if (attr != null) {
                value = attr.getNodeValue();
            }
        }
        return value;
    }

    /**
     * Utility method to set the attribute value on the given
     * element node
     *
     * @param sNode
     * @param attribName
     * @param val
     */
    public static void setAttributeValue(Node sNode, String attribName, String val) {

        Node attr = sNode.getAttributes().getNamedItem(attribName);
        if (attr != null) {
            attr.setNodeValue(val);
        } else {
            attr = sNode.getOwnerDocument().createAttribute(attribName);
            attr.setNodeValue(val);
            sNode.getAttributes().setNamedItem(attr);
        }
    }

    /**
     * Utility method to fetch the value of the element node
     *
     * @param node
     * @return
     */
    public static String getNodeValue(Node node) {
        for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
            if (child.getNodeType() == Node.TEXT_NODE) {
                return child.getNodeValue();
            }
        }
        return null;
    }

    /**
     * Parse & resolve file level alias
     */
    public static Document parseAndResolveAlias(String filename, String locale, boolean ignoreError)
        throws RuntimeException {
        // Force filerefs to be URI's if needed: note this is independent of any other files
        String docURI = filenameToURL(filename);
        Document doc = parse(new InputSource(docURI), filename, ignoreError);
        NodeList elements = doc.getElementsByTagName(LDMLConstants.IDENTITY);
        if (elements.getLength() == 1) {
            Node id = elements.item(0);
            Node sib = id;
            while ((sib = sib.getNextSibling()) != null) {
                if (sib.getNodeType() != Node.ELEMENT_NODE) {
                    continue;
                }
                if (sib.getNodeName().equals(LDMLConstants.ALIAS)) {
                    resolveAliases(doc, filename.substring(0, filename.lastIndexOf(File.separator) + 1), locale, false,
                        null);
                }
            }
        } else {
            System.out.println("Error: elements returned more than 1 identity element!");
        }
        if (DEBUG) {
            try {
                java.io.OutputStreamWriter writer = new java.io.OutputStreamWriter(
                    new java.io.FileOutputStream("./" + File.separator + locale
                        + "_debug_1.xml"), "UTF-8");
                LDMLUtilities.printDOMTree(doc, new PrintWriter(writer),
                    "http://www.unicode.org/cldr/dtd/1.3/ldml.dtd", null);
                writer.flush();
            } catch (IOException e) {
                // throw the exceptionaway .. this is for debugging
            }
        }
        return doc;
    }

    /**
     * Simple worker method to parse filename to a Document.
     *
     * Attempts XML parse, then HTML parse (when parser available),
     * then just parses as text and sticks into a text node.
     *
     * @param filename
     *            to parse as a local path
     *
     * @return Document object with contents of the file;
     *         otherwise throws an unchecked RuntimeException if there
     *         is any fatal problem
     */
    public static Document parse(String filename, boolean ignoreError) throws RuntimeException {
        // Force filerefs to be URI's if needed: note this is independent of any other files
        String docURI = filenameToURL(filename);
        return parse(new InputSource(docURI), filename, ignoreError);
    }

    public static Document parseAndResolveAliases(String locale, String sourceDir, boolean ignoreError,
        boolean ignoreDraft) {
        try {
            Document full = parse(sourceDir + File.separator + locale, ignoreError);
            if (full != null) {
                full = resolveAliases(full, sourceDir, locale, ignoreDraft, null);
            }
            /*
             * Debugging
             *
             * Node[] list = getNodeArray(full, LDMLConstants.ALIAS);
             * if(list.length>0){
             * System.err.println("Aliases not resolved!. list.getLength() returned "+ list.length);
             * }
             */
            return full;
        } catch (Exception ex) {
            if (!ignoreError) {
                ex.printStackTrace();
                throw new RuntimeException(ex);
            }
        }
        return null;

    }

    private static ErrorHandler getNullErrorHandler(final String filename2, final boolean ignoreError) {
        // Local class: cheap non-printing ErrorHandler
        // This is used to suppress validation warnings
        ErrorHandler nullHandler = new ErrorHandler() {
            public void warning(SAXParseException e) throws SAXException {
                int col = e.getColumnNumber();
                String msg = (filename2 + ":" + e.getLineNumber()
                    + (col >= 0 ? ":" + col : "") + ": WARNING: "
                    + e.getMessage());

                System.err.println(msg);
                if (!ignoreError) {
                    throw new RuntimeException(msg);
                }
            }

            public void error(SAXParseException e) throws SAXException {
                int col = e.getColumnNumber();
                String msg = (filename2 + ":" + e.getLineNumber()
                    + (col >= 0 ? ":" + col : "") + ": ERROR: "
                    + e.getMessage());
                System.err.println(msg);
                if (!ignoreError) {
                    throw new RuntimeException(msg);
                }
            }

            public void fatalError(SAXParseException e) throws SAXException {
                throw e;
            }
        };
        return nullHandler;
    }

    public static Document newDocument() {
        return newDocumentBuilder(false).newDocument();
    }

    private static DocumentBuilder newDocumentBuilder(boolean validating) {
        try
        {
            DocumentBuilderFactory dfactory = DocumentBuilderFactory.newInstance();
            // Always set namespaces on
            dfactory.setNamespaceAware(true);
            dfactory.setValidating(validating);
            dfactory.setIgnoringComments(false);
            dfactory.setExpandEntityReferences(true);
            // Set other attributes here as needed
            // applyAttributes(dfactory, attributes);

            DocumentBuilder docBuilder = dfactory.newDocumentBuilder();
            docBuilder.setEntityResolver(new CachingEntityResolver());
            return docBuilder;
        } catch (Throwable se) {
            System.err.println(": ERROR : trying to create documentBuilder: " + se.getMessage());
            se.printStackTrace();
            throw new RuntimeException(se);
        }
    }

    public static Document parse(InputSource docSrc, String filename, boolean ignoreError) {
        Document doc = null;
        try
        {
            // First, attempt to parse as XML (preferred)...
            DocumentBuilder docBuilder = newDocumentBuilder(true);
            docBuilder.setErrorHandler(getNullErrorHandler(filename, ignoreError));
            doc = docBuilder.parse(docSrc);
        } catch (Throwable se)
        {
            // ... if we couldn't parse as XML, attempt parse as HTML...
            System.err.println(filename + ": ERROR :" + se.getMessage());
            se.printStackTrace();
            if (!ignoreError) {
                throw new RuntimeException(se);
            }
        }
        return doc;
    } // end of parse()

    /*
     * Utility method to translate a String filename to URL.
     *
     * Note: This method is not necessarily proven to get the
     * correct URL for every possible kind of filename; it should
     * be improved. It handles the most common cases that we've
     * encountered when running Conformance tests on Xalan.
     * Also note, this method does not handle other non-file:
     * flavors of URLs at all.
     *
     * If the name is null, return null.
     * If the name starts with a common URI scheme (namely the ones
     * found in the examples of RFC2396), then simply return the
     * name as-is (the assumption is that it's already a URL)
     * Otherwise we attempt (cheaply) to convert to a file:/// URL.
     */
    public static String filenameToURL(String filename) {
        // null begets null - something like the commutative property
        if (null == filename) {
            return null;
        }

        // Don't translate a string that already looks like a URL
        if (filename.startsWith("file:")
            || filename.startsWith("http:")
            || filename.startsWith("ftp:")
            || filename.startsWith("gopher:")
            || filename.startsWith("mailto:")
            || filename.startsWith("news:")
            || filename.startsWith("telnet:")) {
            return filename;
        }

        File f = new File(filename);
        String tmp = null;
        try {
            // This normally gives a better path
            tmp = f.getCanonicalPath();
        } catch (IOException ioe) {
            // But this can be used as a backup, for cases
            // where the file does not exist, etc.
            tmp = f.getAbsolutePath();
        }

        // URLs must explicitly use only forward slashes
        if (File.separatorChar == '\\') {
            tmp = tmp.replace('\\', '/');
        }
        // Note the presumption that it's a file reference
        // Ensure we have the correct number of slashes at the
        // start: we always want 3 /// if it's absolute
        // (which we should have forced above)
        if (tmp.startsWith("/")) {
            return "file://" + tmp;
        }
        else {
            return "file:///" + tmp;
        }
    }

    /**
     * Debugging method for printing out the DOM Tree
     * Prints the specified node, recursively.
     *
     * @param node
     * @param out
     * @throws IOException
     */
    public static void printDOMTree(Node node, PrintWriter out, String docType, String copyright) throws IOException
    {
        try {
            Transformer transformer = TransformerFactory.newInstance().newTransformer();
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            transformer.setOutputProperty(OutputKeys.METHOD, "xml");
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");

            transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
            out.print("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n");
            if (copyright != null) {
                out.print(copyright);
            }
            if (docType != null) {
                out.print(docType);
            }

            // transformer.setParameter(entityName, entityRef );
            // transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, XLIFF_PUBLIC_NAME);
            transformer.transform(new DOMSource(node), new StreamResult(out));
        } catch (TransformerException te) {
            throw new IOException(te.getMessage());
        }

        // out.close();
    } // printDOMTree(Node, PrintWriter)

    // Utility functions, HTML and such.
    public static String CVSBASE = "http://www.unicode.org/cldr/trac/browser/trunk";

    public static final String getCVSLink(String locale)
    {
        return "<a href=\"" + CVSBASE + "/common/main/" + locale + ".xml\">";
    }

    public static final String getCVSLink(String locale, String version)
    {
        return "<a href=\"" + CVSBASE + "/common/main/" + locale + ".xml?rev=" +
            version + "\">";

    }

    /**
     * Load the revision from CVS or from the Identity element.
     *
     * @param fileName
     * @return
     */
    static public String loadFileRevision(String fileName)
    {
        int index = fileName.lastIndexOf(File.separatorChar);
        if (index == -1) {
            return null;
        }
        String sourceDir = fileName.substring(0, index);
        return loadFileRevision(sourceDir, new File(fileName).getName());
    }

    // //ldml[@version="1.7"]/identity/version[@number="$Revision: 12133 $"]
    // private static Pattern VERSION_PATTERN =
    // PatternCache.get("//ldml[^/]*/identity/version\\[@number=\"[^0-9]*\\([0-9.]+\\).*");
    private static Pattern VERSION_PATTERN = PatternCache.get(".*identity/version.*Revision[: ]*([0-9.]*).*");

    /**
     * Load the revision from CVS or from the Identity element.
     *
     * @param sourceDir
     * @param fileName
     * @return
     */
    static public String loadFileRevision(String sourceDir, String fileName) {
        String aVersion = null;
        File entriesFile = new File(sourceDir + File.separatorChar + "CVS", "Entries");
        if (entriesFile.exists() && entriesFile.canRead()) {
            try {
                BufferedReader r = new BufferedReader(new FileReader(entriesFile.getPath()));
                String s;
                while ((s = r.readLine()) != null) {
                    String lookFor = "/" + fileName + "/";
                    if (s.startsWith(lookFor)) {
                        String ver = s.substring(lookFor.length());
                        ver = ver.substring(0, ver.indexOf('/'));
                        aVersion = ver;
                    }
                }
                r.close();
            } catch (Throwable th) {
                System.err.println(th.toString() + " trying to read CVS Entries file " + entriesFile.getPath());
                return null;
            }
        } else {
            // no CVS, use file ident.
            File xmlFile = new File(sourceDir, fileName);
            if (!xmlFile.exists()) return null;
            final String bVersion[] = { "unknown" };
            try {
                XMLFileReader xfr = new XMLFileReader().setHandler(new SimpleHandler() {
                    private boolean done = false;

                    // public void handleAttributeDecl(String eName, String aName, String type, String mode, String
                    // value) {
                    public void handlePathValue(String p, String v) {
                        if (!done) {
                            Matcher m = VERSION_PATTERN.matcher(p);
                            if (m.matches()) {
                                // System.err.println("Matches! "+p+" = "+m.group(1));
                                bVersion[0] = m.group(1);
                                done = true;
                            }
                        }
                    }
                });
                xfr.read(xmlFile.getPath(), -1, true);
                aVersion = bVersion[0]; // copy from input param
            } catch (Throwable t) {
                t.printStackTrace();
                aVersion = "err";
                System.err.println("Error reading version of " + xmlFile.getAbsolutePath() + ": " + t.toString());
            }
        }
        // System.err.println("v="+aVersion);
        return aVersion;
    }

    // // Caching Resolution
    // private static File getCacheName(String sourceDir, String last, String loc)
    // {
    // //File xCacheDir = new
    // File((CachingEntityResolver.getCacheDir()!=null)?CachingEntityResolver.getCacheDir():"/tmp/cldrres");
    // return new File(sourceDir).getName() + "_" + last + "." + loc;
    // }

    // Document readMergeCache(String sourceDir, String last, String loc)
    // {
    // File cacheName = getCacheName(String sourceDir, last, loc);
    // System.out.println(" M:  " + cacheName);
    // File cacheFile = new File(xCacheDir, cacheName + ".xml");
    // if(cacheFile.exists()) { // && is newer than last, loc
    // doc = parse(cacheFile.getPath(),ignoreUnavailable);
    // }
    // if(doc!=null) {
    // System.out.println("Cache hit for " + cacheName);
    // }
    // return doc;
    // }

    // void writeMergeCache(String sourceDir, String last, String loc, Document full)
    // {
    // try {
    // OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(cacheFile),"UTF-8");
    // PrintWriter pw = new PrintWriter(writer);
    // printDOMTree(full,pw);
    // long stop = System.currentTimeMillis();
    // long total = (stop-start);
    // double s = total/1000.;
    // System.out.println(" " + cacheName + " parse time: " + s + "s");
    // pw.println("<!-- " + cacheName + " parse time: " + s + "s -->");
    // writer.close();
    // } catch (Throwable t) {
    // System.err.println(t.toString() + " while trying to write cache file " + cacheName);
    // }
    // }

    public static String getFullPath(int fileType, String fName, String dir) {
        String str = null;
        int lastIndex1 = fName.lastIndexOf(File.separator, fName.length()) + 1/* add 1 to skip past the separator */;
        int lastIndex2 = fName.lastIndexOf('.', fName.length());
        if (fileType == TXT) {
            if (lastIndex2 == -1) {
                fName = fName.trim() + ".txt";
            } else {
                if (!fName.substring(lastIndex2).equalsIgnoreCase(".txt")) {
                    fName = fName.substring(lastIndex1, lastIndex2) + ".txt";
                }
            }
            if (dir != null && fName != null) {
                str = dir + "/" + fName.trim();
            } else {
                str = System.getProperty("user.dir") + "/" + fName.trim();
            }
        } else if (fileType == XML) {
            if (lastIndex2 == -1) {
                fName = fName.trim() + ".xml";
            } else {
                if (!fName.substring(lastIndex2).equalsIgnoreCase(".xml")
                    && fName.substring(lastIndex2).equalsIgnoreCase(".xlf")) {
                    fName = fName.substring(lastIndex1, lastIndex2) + ".xml";
                }
            }
            if (dir != null && fName != null) {
                str = dir + "/" + fName;
            } else if (lastIndex1 > 0) {
                str = fName;
            } else {
                str = System.getProperty("user.dir") + "/" + fName;
            }
        } else {
            System.err.println("Invalid file type.");
            System.exit(-1);
        }
        return str;
    }

    /**
     * split an alt= tag into pieces. Any piece can be missing (== null)
     * Piece 0: 'alt type'. null means this is the normal (non-alt) item. Possible values are 'alternate', 'colloquial',
     * etc.
     * Piece 1: 'proposed type'. If non-null, this is a string beginning with 'proposed' and containing arbitrary other
     * text.
     *
     * STRING 0 1
     * -------------------------------
     * ""/null null null
     * something something null
     * something-proposed something proposed
     * something-proposed3 something proposed3
     * proposed null proposed
     * somethingproposed somethingproposed null
     *
     *
     * @param alt
     *            the alt tag to parse
     * @return a 2-element array containing piece 0 and piece 1
     * @see formatAlt
     */
    public static String[] parseAlt(String alt) {
        String[] ret = new String[2];
        if (alt == null) {
            ret[0] = null;
            ret[1] = null;
        } else {
            int l = alt.indexOf(LDMLConstants.PROPOSED);
            if (l == -1) { /* no PROPOSED */
                ret[0] = alt; // all alt,
                ret[1] = null; // no kind
            } else if (l == 0) { /* begins with */
                ret[0] = null; // all properties
                ret[1] = alt;
            } else {
                if (alt.charAt(l - 1) != '-') {
                    throw new InternalError("Expected '-' before " + LDMLConstants.PROPOSED + " in " + alt);
                }
                ret[0] = alt.substring(0, l - 1);
                ret[1] = alt.substring(l);

                if (ret[0].length() == 0) {
                    ret[0] = null;
                }
                if (ret[1].length() == 0) {
                    ret[1] = null;
                }
            }
        }
        return ret;
    }

    /**
     * Format aan alt string from components.
     *
     * @param altType
     *            optional alternate type (i.e. 'alternate' or 'colloquial').
     * @param proposedType
     * @see parseAlt
     */
    public static String formatAlt(String altType, String proposedType) {

        if (((altType == null) || (altType.length() == 0)) &&
            ((proposedType == null) || (proposedType.length() == 0))) {
            return null;
        }

        if ((proposedType == null) || (proposedType.length() == 0)) {
            return altType; // no proposed type: 'alternate'
        } else if (!proposedType.startsWith(LDMLConstants.PROPOSED)) {
            throw new InternalError("proposedType must begin with " + LDMLConstants.PROPOSED);
        }

        if ((altType == null) || (altType.length() == 0)) {
            return proposedType; // Just a proposed type: "proposed" or "proposed-3"
        } else {
            return altType + "-" + proposedType; // 'alternate-proposed'
        }
    }

    /**
     * Compatibility.
     *
     * @param node
     * @param xpath
     * @return
     * @throws TransformerException
     */
    private static NodeList XPathAPI_selectNodeList(Node node, String xpath) throws TransformerException {
        XPathFactory factory = XPathFactory.newInstance();
        XPath xPath = factory.newXPath();
        setNamespace(xPath, xpath);
        try {
            XPathExpression xPathExpression =
                xPath.compile(xpath);
            return (NodeList) xPathExpression.evaluate(node, XPathConstants.NODESET);
        } catch (XPathExpressionException e) {
            throw new TransformerException("Exception in XPathAPI_selectNodeList: " + xpath, e);
        }
    }

    private static NodeList XPathAPI_selectNodeList(Document doc, String xpath,
        Node namespaceNode) throws TransformerException {
        XPathFactory factory = XPathFactory.newInstance();
        XPath xPath = factory.newXPath();
        setNamespace(xPath, xpath);
        try {
            XPathExpression xPathExpression =
                xPath.compile(xpath);
            return (NodeList) xPathExpression.evaluate(doc, XPathConstants.NODESET);
        } catch (XPathExpressionException e) {
            throw new TransformerException("Exception in XPathAPI_selectNodeList: " + xpath, e);
        }
    }

    private static NodeList XPathAPI_selectNodeList(Node context, String xpath,
        Node namespaceNode) throws TransformerException {
        XPathFactory factory = XPathFactory.newInstance();
        XPath xPath = factory.newXPath();
        setNamespace(xPath, xpath);
        try {
            XPathExpression xPathExpression =
                xPath.compile(xpath);
            return (NodeList) xPathExpression.evaluate(context, XPathConstants.NODESET);
        } catch (XPathExpressionException e) {
            throw new TransformerException("Exception in XPathAPI_selectNodeList: " + xpath, e);
        }
    }

    private static void XPathAPI_eval(Node context, String string,
        Node namespaceNode) throws TransformerException {
        XPathAPI_selectNodeList(context, string, namespaceNode);
    }

    private static void XPathAPI_eval(Node context, String string) throws TransformerException {
        XPathAPI_selectNodeList(context, string);
    }

    private static void setNamespace(XPath xpath, String string) {
        if (string.contains("special/icu:")) {
            xpath.setNamespaceContext(new javax.xml.namespace.NamespaceContext() {
                public String getNamespaceURI(String prefix) {
                    if (prefix.equals("icu")) {
                        return "http://www.icu-project.org";
                    }
                    return null;
                }

                public String getPrefix(String namespaceURI) {
                    return null;
                }

                public Iterator<Object> getPrefixes(String namespaceURI) {
                    return null;
                }
            });
        }
    }
}
