com.netspective.sparx.util.xml.XmlSource.java Source code

Java tutorial

Introduction

Here is the source code for com.netspective.sparx.util.xml.XmlSource.java

Source

/*
 * Copyright (c) 2000-2002 Netspective Corporation -- all rights reserved
 *
 * Netspective Corporation permits redistribution, modification and use
 * of this file in source and binary form ("The Software") under the
 * Netspective Source License ("NSL" or "The License"). The following
 * conditions are provided as a summary of the NSL but the NSL remains the
 * canonical license and must be accepted before using The Software. Any use of
 * The Software indicates agreement with the NSL.
 *
 * 1. Each copy or derived work of The Software must preserve the copyright
 *    notice and this notice unmodified.
 *
 * 2. Redistribution of The Software is allowed in object code form only
 *    (as Java .class files or a .jar file containing the .class files) and only
 *    as part of an application that uses The Software as part of its primary
 *    functionality. No distribution of the package is allowed as part of a software
 *    development kit, other library, or development tool without written consent of
 *    Netspective Corporation. Any modified form of The Software is bound by
 *    these same restrictions.
 *
 * 3. Redistributions of The Software in any form must include an unmodified copy of
 *    The License, normally in a plain ASCII text file unless otherwise agreed to,
 *    in writing, by Netspective Corporation.
 *
 * 4. The names "Sparx" and "Netspective" are trademarks of Netspective
 *    Corporation and may not be used to endorse products derived from The
 *    Software without without written consent of Netspective Corporation. "Sparx"
 *    and "Netspective" may not appear in the names of products derived from The
 *    Software without written consent of Netspective Corporation.
 *
 * 5. Please attribute functionality to Sparx where possible. We suggest using the
 *    "powered by Sparx" button or creating a "powered by Sparx(tm)" link to
 *    http://www.netspective.com for each application using Sparx.
 *
 * The Software is provided "AS IS," without a warranty of any kind.
 * ALL EXPRESS OR IMPLIED REPRESENTATIONS AND WARRANTIES, INCLUDING ANY
 * IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
 * OR NON-INFRINGEMENT, ARE HEREBY DISCLAIMED.
 *
 * NETSPECTIVE CORPORATION AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES
 * SUFFERED BY LICENSEE OR ANY THIRD PARTY AS A RESULT OF USING OR DISTRIBUTING
 * THE SOFTWARE. IN NO EVENT WILL NETSPECTIVE OR ITS LICENSORS BE LIABLE
 * FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL,
 * CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND
 * REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR
 * INABILITY TO USE THE SOFTWARE, EVEN IF HE HAS BEEN ADVISED OF THE POSSIBILITY
 * OF SUCH DAMAGES.
 *
 * @author Shahid N. Shah
 */

/**
 * $Id: XmlSource.java,v 1.13 2003-01-12 22:36:36 shahid.shah Exp $
 */

package com.netspective.sparx.util.xml;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.Arrays;

import javax.servlet.ServletContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamSource;

import org.apache.xpath.XPathAPI;
import org.apache.oro.text.regex.*;
import org.apache.oro.text.perl.Perl5Util;
import org.apache.commons.jexl.Expression;
import org.apache.commons.jexl.ExpressionFactory;
import org.apache.commons.jexl.JexlContext;
import org.apache.commons.jexl.JexlHelper;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;

import com.netspective.sparx.util.metric.Metric;
import com.netspective.sparx.util.ClassPath;

public class XmlSource {
    public class SourceInfo {
        protected File source;
        protected long lastModified;
        protected SourceInfo parent;
        protected List preProcessors;

        public SourceInfo(SourceInfo includedFrom, File file) {
            source = file;
            lastModified = source.lastModified();
            parent = includedFrom;
        }

        public File getFile() {
            return source;
        }

        public SourceInfo getParent() {
            return parent;
        }

        public List getPreProcessors() {
            return preProcessors;
        }

        public void addPreProcessor(SourceInfo value) {
            if (preProcessors == null)
                preProcessors = new ArrayList();
            preProcessors.add(value);
        }

        public boolean sourceChanged() {
            if (source.lastModified() > this.lastModified)
                return true;

            if (preProcessors != null) {
                for (int i = 0; i < preProcessors.size(); i++) {
                    if (((SourceInfo) preProcessors.get(i)).sourceChanged())
                        return true;
                }
            }

            return false;
        }
    }

    protected boolean allowReload = true;
    protected ArrayList errors = new ArrayList();
    protected SourceInfo docSource;
    protected Map sourceFiles = new HashMap();
    protected Document xmlDoc;
    protected Element metaInfoElem;
    protected Element metaInfoOptionsElem;
    protected Set inheritanceHistorySet = new HashSet();
    protected Set defaultExcludeElementsFromInherit = new HashSet();
    protected Map templates = new HashMap();
    protected String catalogedNodeIdentifiersClassName;

    public static void defineClassAttributes(Element defnElement, Class cls, String attrPrefix) {
        if (cls == null)
            return;

        String className = cls.getName();
        String classFileName = ClassPath.getClassFileName(className);

        defnElement.setAttribute(attrPrefix + "class-name", className);
        defnElement.setAttribute(attrPrefix + "class-file-name", classFileName);

        String classSrcName = classFileName.substring(0, classFileName.lastIndexOf('.')) + ".java";
        if (new File(classSrcName).exists())
            defnElement.setAttribute(attrPrefix + "class-src-name", classSrcName);
    }

    /**
     * Return the given text unindented by whatever the first line is indented by
     * @param text The original text
     * @return Unindented text or original text if not indented
     */
    public static String getUnindentedText(String text) {
        /*
         * if the entire SQL string is indented, find out how far the first line is indented
         */
        StringBuffer replStr = new StringBuffer();
        for (int i = 0; i < text.length(); i++) {
            char ch = text.charAt(i);
            if (Character.isWhitespace(ch))
                replStr.append(ch);
            else
                break;
        }

        /*
         * If the first line is indented, unindent all the lines the distance of just the first line
         */
        Perl5Util perlUtil = new Perl5Util();

        if (replStr.length() > 0)
            return perlUtil.substitute("s/" + replStr + "/\n/g", text).trim();
        else
            return text;
    }

    /**
     * Return the given text indented by the given string
     * @param text The original text
     * @return Unindented text or original text if not indented
     */
    public static String getIndentedText(String text, String indent, boolean appendNewLine) {
        text = getUnindentedText(text);

        /*
         * If the first line is indented, unindent all the lines the distance of just the first line
         */
        Perl5Util perlUtil = new Perl5Util();
        text = perlUtil.substitute("s/^/" + indent + "/gm", text);
        return appendNewLine ? text + "\n" : text;
    }

    public boolean getAllowReload() {
        return allowReload;
    }

    public void setAllowReload(boolean value) {
        allowReload = value;
    }

    public void initializeForServlet(ServletContext servletContext) {
        if (com.netspective.sparx.util.config.ConfigurationManagerFactory
                .isProductionOrTestEnvironment(servletContext))
            setAllowReload(false);
    }

    /**
     * returns the boolean equivalent of a string, which is considered true
     * if either "on", "true", or "yes" is found, ignoring case.
     */
    public static boolean toBoolean(String s) {
        return (s.equalsIgnoreCase("on") || s.equalsIgnoreCase("true") || s.equalsIgnoreCase("yes"));
    }

    /**
     * Given a text string, return a string that would be suitable for that string to be used
     * as a Java identifier (as a variable or method name). Depending upon whether ucaseInitial
     * is set, the string starts out with a lowercase or uppercase letter. Then, the rule is
     * to convert all periods into underscores and title case any words separated by
     * underscores. This has the effect of removing all underscores and creating mixed case
     * words. For example, Person_Address becomes personAddress or PersonAddress depending upon
     * whether ucaseInitial is set to true or false. Person.Address would become Person_Address.
     */
    public static String xmlTextToJavaIdentifier(String xml, boolean ucaseInitial) {
        if (xml == null || xml.length() == 0)
            return xml;

        StringBuffer identifier = new StringBuffer();
        char ch = xml.charAt(0);
        identifier.append(ucaseInitial ? Character.toUpperCase(ch) : Character.toLowerCase(ch));

        boolean uCase = false;
        for (int i = 1; i < xml.length(); i++) {
            ch = xml.charAt(i);
            if (ch == '.') {
                identifier.append('_');
            } else if (ch != '_' && Character.isJavaIdentifierPart(ch)) {
                identifier.append(Character.isUpperCase(ch) ? ch
                        : (uCase ? Character.toUpperCase(ch) : Character.toLowerCase(ch)));
                uCase = false;
            } else
                uCase = true;
        }
        return identifier.toString();
    }

    /**
     * Given a text string, return a string that would be suitable for that string to be used
     * as a Java constant (public static final XXX). The rule is to basically take every letter
     * or digit and return it in uppercase and every non-letter or non-digit as an underscore.
     */
    public static String xmlTextToJavaConstant(String xml) {
        if (xml == null || xml.length() == 0)
            return xml;

        StringBuffer constant = new StringBuffer();
        for (int i = 0; i < xml.length(); i++) {
            char ch = xml.charAt(i);
            constant.append(Character.isJavaIdentifierPart(ch) ? Character.toUpperCase(ch) : '_');
        }
        return constant.toString();
    }

    /**
     * Given a text string, return a string that would be suitable for that string to be used
     * as a Java constant (public static final XXX). The rule is to basically take every letter
     * or digit and return it in uppercase and every non-letter or non-digit as an underscore.
     * This trims all non-letter/digit characters from the beginning of the string.
     */
    public static String xmlTextToJavaConstantTrimmed(String xml) {
        if (xml == null || xml.length() == 0)
            return xml;

        boolean stringStarted = false;
        StringBuffer constant = new StringBuffer();
        for (int i = 0; i < xml.length(); i++) {
            char ch = xml.charAt(i);
            if (Character.isJavaIdentifierPart(ch)) {
                stringStarted = true;
                constant.append(Character.toUpperCase(ch));
            } else if (stringStarted)
                constant.append('_');
        }
        return constant.toString();
    }

    /**
     * Given a text string, return a string that would be suitable for an XML element name. For example,
     * when given Person_Address it would return person-address. The rule is to basically take every letter
     * or digit and return it in lowercase and every non-letter or non-digit as a dash.
     */
    public static String xmlTextToNodeName(String xml) {
        if (xml == null || xml.length() == 0)
            return xml;

        StringBuffer constant = new StringBuffer();
        for (int i = 0; i < xml.length(); i++) {
            char ch = xml.charAt(i);
            constant.append(Character.isLetterOrDigit(ch) ? Character.toLowerCase(ch) : '-');
        }
        return constant.toString();
    }

    /**
     * Given a attribute or tag name, find the item or return the defaultText if there is none.
     */
    public static String getAttrValueOrTagText(Element parent, String name, String defaultText) {
        String attrValue = parent.getAttribute(name);
        return attrValue.length() > 0 ? attrValue : getTagText(parent, name, defaultText);
    }

    /**
     * Given a attribute or tag name, find the item or return the defaultValue
     */
    public static String getAttrValueOrDefault(Element parent, String name, String defaultValue) {
        String attrValue = parent.getAttribute(name);
        return attrValue.length() > 0 ? attrValue : getTagText(parent, name, defaultValue);
    }

    /**
     * Given an attribute name, check to see if it has a value -- if not, set it to default
     */
    public static void setAttrValueDefault(Element parent, String name, String defaultValue) {
        String attrValue = parent.getAttribute(name);
        if (attrValue.length() == 0 && defaultValue != null && defaultValue.length() > 0)
            parent.setAttribute(name, defaultValue);
    }

    public static Element getOrCreateElement(Element parent, String name) {
        NodeList nl = parent.getElementsByTagName(name);
        if (nl.getLength() == 0) {
            Element newElem = parent.getOwnerDocument().createElement(name);
            parent.appendChild(newElem);
            return newElem;
        } else
            return (Element) nl.item(0);
    }

    /**
     * Given a tag, find the tag in the parent element and return its text or the default String if there is none.
     */
    public static String getTagText(Element parent, String tag, String defaultText) {
        NodeList nl = parent.getElementsByTagName(tag);

        if (nl.getLength() == 0)
            return defaultText;

        StringBuffer text = new StringBuffer();
        for (int i = 0; i < nl.getLength(); i++) {
            Element tagElem = (Element) nl.item(i);
            NodeList children = tagElem.getChildNodes();
            for (int c = 0; c < children.getLength(); c++) {
                text.append(children.item(c).getNodeValue());
            }
        }
        return text.toString();
    }

    /**
     * This class accepts a list and a pattern and creates a second list with the items
     * that match the pattern. If the pattern is "*" then all items are matched. If the pattern is a valid ORO Perl5
     * regular expression, the expression is used to do the matching. If it's not "*" or a perl expression, then the
     * simple pattern is returned if it's found in the list.
     */
    public static class StringListMatcher {
        private PatternCompiler compiler = new Perl5Compiler();
        private PatternMatcher matcher = new Perl5Matcher();
        private Pattern pattern;
        private MalformedPatternException patternException;

        private List source;
        private List dest = new ArrayList();

        public StringListMatcher(List source, String pattern) {
            this.source = source;

            if (pattern.equals("*")) {
                dest.addAll(source);
            } else if (pattern.startsWith("/") && pattern.endsWith("/")) {
                String actualPattern = pattern.substring(1, pattern.length() - 2);
                try {
                    this.pattern = compiler.compile(actualPattern, Perl5Compiler.CASE_INSENSITIVE_MASK);
                    for (int i = 0; i < source.size(); i++) {
                        String item = (String) source.get(i);
                        if (matcher.contains(item, this.pattern))
                            dest.add(item);
                    }
                } catch (MalformedPatternException e) {
                    patternException = e;
                    dest.add(pattern);
                }
            } else
                dest.add(pattern);
        }

        public List getMatchedItems() {
            return dest;
        }

        public MalformedPatternException getPatternException() {
            return patternException;
        }

        public List getSource() {
            return source;
        }

        public Pattern getPattern() {
            return pattern;
        }
    }

    public Document getDocument() {
        reload();
        return xmlDoc;
    }

    /**
     * Given an element, see if the element is a <templates> element. If it is, then catalog all of
     * the elements as templates that can be re-used at a later point.
     */
    public void catalogElement(Element elem) {
        if (!"templates".equals(elem.getNodeName()))
            return;

        String pkgName = elem.getAttribute("package");

        NodeList children = elem.getChildNodes();
        for (int c = 0; c < children.getLength(); c++) {
            Node child = children.item(c);
            if (child.getNodeType() != Node.ELEMENT_NODE)
                continue;

            Element childElem = (Element) child;
            String templateName = childElem.getAttribute("id");
            if (templateName.length() == 0)
                templateName = childElem.getAttribute("name");

            templates.put(pkgName.length() > 0 ? (pkgName + "." + templateName) : templateName, childElem);
        }
    }

    /**
     * Given an element, apply templates to the node. If there is an attribute called "template" then inherit that
     * template first. Then, search through all of the nodes in the element and try to find all <include-template id="x">
     * elements to copy the template elements at those locations. Also, go through each child to see if a tag name
     * exists that matches a template name -- if it does, then "inherit" that template to replace the element at that
     * location.
     */
    public void processTemplates(Element elem) {
        inheritNodes(elem, templates, "template", defaultExcludeElementsFromInherit);

        NodeList includes = elem.getElementsByTagName("include-template");
        if (includes != null && includes.getLength() > 0) {
            for (int n = 0; n < includes.getLength(); n++) {
                Element include = (Element) includes.item(n);
                String templateName = include.getAttribute("id");
                Element template = (Element) templates.get(templateName);

                if (template != null) {
                    NodeList incChildren = template.getChildNodes();
                    for (int c = 0; c < incChildren.getLength(); c++) {
                        Node incCopy = xmlDoc.importNode(incChildren.item(c), true);
                        if (incCopy.getNodeType() == Node.ELEMENT_NODE)
                            ((Element) incCopy).setAttribute("_included-from-template", templateName);
                        elem.insertBefore(incCopy, include);
                    }
                }
            }
        }

        NodeList children = elem.getChildNodes();
        for (int c = 0; c < children.getLength(); c++) {
            Node childNode = children.item(c);
            if (childNode.getNodeType() != Node.ELEMENT_NODE)
                continue;

            String nodeName = childNode.getNodeName();
            if (templates.containsKey(nodeName)) {
                Element template = (Element) templates.get(nodeName);
                Node incCopy = xmlDoc.importNode(template, true);
                if (incCopy.getNodeType() == Node.ELEMENT_NODE)
                    ((Element) incCopy).setAttribute("_included-from-template", nodeName);

                // make sure that the child's attributes overwrite the attributes in the templates with the same name
                NamedNodeMap attrsInChild = childNode.getAttributes();
                for (int a = 0; a < attrsInChild.getLength(); a++) {
                    Node childAttr = attrsInChild.item(a);
                    ((Element) incCopy).setAttribute(childAttr.getNodeName(), childAttr.getNodeValue());
                }

                // now do the actual replacement
                inheritNodes((Element) incCopy, templates, "template", defaultExcludeElementsFromInherit);
                elem.replaceChild(incCopy, childNode);
            } else
                inheritNodes((Element) childNode, templates, "template", defaultExcludeElementsFromInherit);
        }
    }

    public List getErrors() {
        return errors;
    }

    public void addError(String msg) {
        errors.add(msg);
    }

    public SourceInfo getSourceDocument() {
        return docSource;
    }

    public Map getSourceFiles() {
        return sourceFiles;
    }

    public boolean sourceChanged() {
        if (docSource == null)
            return false;

        if (sourceFiles.size() > 1) {
            for (Iterator i = sourceFiles.values().iterator(); i.hasNext();) {
                if (((SourceInfo) i.next()).sourceChanged())
                    return true;
            }
        } else
            return docSource.sourceChanged();

        // if we get to here, none of the files is newer than what's in memory
        return false;
    }

    public void forceReload() {
        loadDocument(docSource.getFile());
    }

    public void reload() {
        if (allowReload && docSource != null && sourceChanged())
            loadDocument(docSource.getFile());
    }

    public boolean loadDocument(File file) {
        docSource = null;
        xmlDoc = null;
        loadXML(file);
        catalogNodes();
        return errors.size() == 0 ? true : false;
    }

    public String findElementOrAttrValue(Element elem, String nodeName) {
        String attrValue = elem.getAttribute(nodeName);
        if (attrValue.length() > 0)
            return attrValue;

        // we don't usually want the first value -- we want the last in case there is inheritance
        String lastValue = null;

        NodeList children = elem.getChildNodes();
        for (int n = 0; n < children.getLength(); n++) {
            Node node = children.item(n);
            if (node.getNodeName().equals(nodeName))
                lastValue = node.getFirstChild().getNodeValue();
        }

        return lastValue;
    }

    public String ucfirst(String str) {
        return str.substring(0, 1).toUpperCase() + str.substring(1);
    }

    public void inheritElement(Element srcElement, Element destElem, Set excludeElems, String inheritedFromNode) {
        NamedNodeMap inhAttrs = srcElement.getAttributes();
        for (int i = 0; i < inhAttrs.getLength(); i++) {
            Node attrNode = inhAttrs.item(i);
            final String nodeName = attrNode.getNodeName();
            if (!excludeElems.contains(nodeName) && destElem.getAttribute(nodeName).equals(""))
                destElem.setAttribute(nodeName, attrNode.getNodeValue());
        }

        DocumentFragment inheritFragment = xmlDoc.createDocumentFragment();
        NodeList inhChildren = srcElement.getChildNodes();
        for (int i = inhChildren.getLength() - 1; i >= 0; i--) {
            Node childNode = inhChildren.item(i);

            // only add if there isn't an attribute overriding this element
            final String nodeName = childNode.getNodeName();
            if (destElem.getAttribute(nodeName).length() == 0 && (!excludeElems.contains(nodeName))) {
                Node cloned = childNode.cloneNode(true);
                if (inheritedFromNode != null && cloned.getNodeType() == Node.ELEMENT_NODE)
                    ((Element) cloned).setAttribute("_inherited-from", inheritedFromNode);
                inheritFragment.insertBefore(cloned, inheritFragment.getFirstChild());
            }
        }

        destElem.insertBefore(inheritFragment, destElem.getFirstChild());
    }

    public void inheritNodes(Element element, Map sourcePool, String attrName, Set excludeElems) {
        String inheritAttr = element.getAttribute(attrName);
        while (inheritAttr != null && inheritAttr.length() > 0) {
            Element inheritFromElem = null;
            StringTokenizer inheritST = new StringTokenizer(inheritAttr, ",");
            String[] inherits = new String[15];
            int inheritsCount = 0;
            while (inheritST.hasMoreTokens()) {
                inherits[inheritsCount] = inheritST.nextToken();
                inheritsCount++;
            }

            /** we're going to work backwards because we want to make sure the
             *  elements are added in the appropriate order (same order as the
             *  inheritance list)
             */

            for (int j = (inheritsCount - 1); j >= 0; j--) {
                String inheritType = inherits[j];
                inheritFromElem = (Element) sourcePool.get(inheritType);
                if (inheritFromElem == null) {
                    errors.add("can not extend '" + element.getAttribute("name") + "' from '" + inheritType
                            + "': source not found");
                    continue;
                }

                /* don't inherit the same objects more than once */
                String inheritanceId = Integer.toString(element.hashCode()) + '.'
                        + Integer.toString(inheritFromElem.hashCode());
                if (inheritanceHistorySet.contains(inheritanceId)) {
                    errors.add("Attempting to copy duplicate node: " + inheritanceId + ", " + element.getTagName()
                            + ", " + element.getAttribute("name") + ", " + inheritFromElem.getTagName());
                    //continue;
                }
                inheritanceHistorySet.add(inheritanceId);

                Element extendsElem = xmlDoc.createElement("extends");
                extendsElem.appendChild(xmlDoc.createTextNode(inheritType));
                element.appendChild(extendsElem);

                inheritElement(inheritFromElem, element, excludeElems, inheritType);
            }

            // find the next one if we have more parents
            if (inheritFromElem != null)
                inheritAttr = inheritFromElem.getAttribute(attrName);
            else
                inheritAttr = null;
        }
    }

    public void replaceNodeValue(Node node, String findStr, String replStr) {
        String srcStr = node.getNodeValue();
        if (srcStr == null || findStr == null || replStr == null)
            return;

        int findLoc = srcStr.indexOf(findStr);
        if (findLoc >= 0) {
            StringBuffer sb = new StringBuffer(srcStr);
            sb.replace(findLoc, findLoc + findStr.length(), replStr);
            node.setNodeValue(sb.toString());
        }
    }

    public void addMetaInfoOptions() {
        if (xmlDoc == null || metaInfoElem == null)
            return;

        if (metaInfoOptionsElem != null)
            metaInfoElem.removeChild(metaInfoOptionsElem);

        metaInfoOptionsElem = xmlDoc.createElement("options");
        metaInfoOptionsElem.setAttribute("name", "Allow reload");
        metaInfoOptionsElem.setAttribute("value", (allowReload ? "Yes" : "No"));
        metaInfoElem.appendChild(metaInfoOptionsElem);
    }

    public void addMetaInformation() {
        NodeList existing = xmlDoc.getDocumentElement().getElementsByTagName("meta-info");
        if (existing.getLength() > 0) {
            xmlDoc.getDocumentElement().removeChild(existing.item(0));
            metaInfoElem = null;
            metaInfoOptionsElem = null;
        }

        metaInfoElem = xmlDoc.createElement("meta-info");
        xmlDoc.getDocumentElement().appendChild(metaInfoElem);

        addMetaInfoOptions();

        Element filesElem = xmlDoc.createElement("source-files");
        metaInfoElem.appendChild(filesElem);

        for (Iterator sfi = sourceFiles.values().iterator(); sfi.hasNext();) {
            SourceInfo si = (SourceInfo) sfi.next();
            Element fileElem = xmlDoc.createElement("source-file");
            fileElem.setAttribute("abs-path", si.getFile().getAbsolutePath());
            if (si.getParent() != null)
                fileElem.setAttribute("included-from", si.getParent().getFile().getName());
            filesElem.appendChild(fileElem);

            List preProcessors = si.getPreProcessors();
            if (preProcessors != null) {
                for (int i = 0; i < preProcessors.size(); i++) {
                    SourceInfo psi = (SourceInfo) preProcessors.get(i);
                    fileElem = xmlDoc.createElement("source-file");
                    fileElem.setAttribute("abs-path", psi.getFile().getAbsolutePath());
                    if (psi.getParent() != null)
                        fileElem.setAttribute("included-from", psi.getParent().getFile().getName());
                    filesElem.appendChild(fileElem);
                }
            }
        }

        if (errors.size() > 0) {
            Element errorsElem = xmlDoc.createElement("errors");
            metaInfoElem.appendChild(errorsElem);

            for (Iterator ei = errors.iterator(); ei.hasNext();) {
                Element errorElem = xmlDoc.createElement("error");
                Text errorText = xmlDoc.createTextNode((String) ei.next());
                errorElem.appendChild(errorText);
                errorsElem.appendChild(errorElem);
            }
        }
    }

    static public String getClassName(String pkgAndClassName, char sep) {
        int classNameDelimPos = pkgAndClassName.lastIndexOf(sep);
        return classNameDelimPos != -1 ? pkgAndClassName.substring(classNameDelimPos + 1) : pkgAndClassName;
    }

    static public String getPackageName(String pkgAndClassName, char sep) {
        int classNameDelimPos = pkgAndClassName.lastIndexOf(sep);
        return classNameDelimPos != -1 ? pkgAndClassName.substring(0, classNameDelimPos) : null;
    }

    /**
     * Return the list of identifiers that this class has cataloged via catalogNodes. This list is used to
     * store the identifers in a Java class.
     */

    public String[] getCatalogedNodeIdentifiers() {
        return null;
    }

    public String getCatalogedNodeIdentifiersClassName() {
        return catalogedNodeIdentifiersClassName;
    }

    public class NodeIdentifiersClassInfo {
        private String rootPath;
        private String defaultPkgAndClassName;
        private String pkgAndClassName;
        private String[] identifiers;
        private char subPackageSeparator = '.';

        public NodeIdentifiersClassInfo(String rootPath, String defaultPkgAndClassName) {
            this.rootPath = rootPath;
            this.defaultPkgAndClassName = defaultPkgAndClassName;

            identifiers = getCatalogedNodeIdentifiers();
            Arrays.sort(identifiers, String.CASE_INSENSITIVE_ORDER);

            pkgAndClassName = getCatalogedNodeIdentifiersClassName();
            if (pkgAndClassName == null)
                pkgAndClassName = defaultPkgAndClassName;
        }

        public NodeIdentifiersClassInfo(String rootPath, String pkgAndClassName, char subPackageSeparator) {
            this(rootPath, pkgAndClassName);
            this.subPackageSeparator = subPackageSeparator;
        }

        public void generateCode(String subPkgAndClassName, List ids) throws IOException {
            String fullPkgName, className;
            File file;

            if (subPkgAndClassName == null) {
                fullPkgName = getPackageName(pkgAndClassName, '.');
                className = getClassName(pkgAndClassName, '.');
                file = new File(rootPath, pkgAndClassName.replace('.', '/') + ".java");
            } else {
                String subPkgName = getPackageName(subPkgAndClassName, subPackageSeparator);
                if (subPkgName == null)
                    fullPkgName = pkgAndClassName.toLowerCase();
                else
                    fullPkgName = (pkgAndClassName + subPackageSeparator + subPkgName).toLowerCase();
                className = getClassName(subPkgAndClassName, subPackageSeparator);
                String fullClassSpec = fullPkgName + subPackageSeparator + xmlTextToJavaIdentifier(className, true);
                file = new File(rootPath, fullClassSpec.replace(subPackageSeparator, '/') + ".java");
            }

            file.getParentFile().mkdirs();
            FileWriter writer = new FileWriter(file);

            writer.write(
                    "\n/* this file is generated by com.netspective.sparx.util.xml.XmlSource.createNodeIdentifiersClass(), do not modify (you can extend it, though) */\n\n");
            if (fullPkgName != null)
                writer.write("package " + fullPkgName.replace(subPackageSeparator, '.') + ";\n\n");

            writer.write("public class " + xmlTextToJavaIdentifier(className, true) + "\n");
            writer.write("{\n");

            for (int i = 0; i < ids.size(); i++) {
                String identifier = (String) ids.get(i);
                String constant = xmlTextToJavaConstantTrimmed(
                        subPkgAndClassName != null ? identifier.substring(subPkgAndClassName.length() + 1)
                                : identifier);
                if (constant.length() > 0)
                    writer.write("    static public final String " + constant + " = \"" + identifier + "\";\n");
                else
                    writer.write("    // static public final String " + constant + " = \"" + identifier + "\";\n");
            }

            writer.write("}\n");
            writer.close();
        }

        public void generateCode() throws IOException {
            List idsWithNoPackages = new ArrayList();
            Map idsInPackages = new HashMap();

            for (int i = 0; i < identifiers.length; i++) {
                String identifier = identifiers[i];
                if (identifier.indexOf(subPackageSeparator) > 0) {
                    String packageName = identifier.substring(0, identifier.lastIndexOf(subPackageSeparator));
                    List ids = (List) idsInPackages.get(packageName);
                    if (ids == null) {
                        ids = new ArrayList();
                        idsInPackages.put(packageName, ids);
                    }
                    ids.add(identifier);
                } else if (identifier.length() > 0)
                    idsWithNoPackages.add(identifier);
            }

            if (idsWithNoPackages.size() > 0)
                generateCode(null, idsWithNoPackages);

            Iterator idsInPackage = idsInPackages.entrySet().iterator();
            while (idsInPackage.hasNext()) {
                Map.Entry entry = (Map.Entry) idsInPackage.next();
                String subPackageName = ((String) entry.getKey()).toLowerCase();
                List ids = (List) entry.getValue();
                generateCode(subPackageName, ids);
            }
        }

        public String getDefaultPkgAndClassName() {
            return defaultPkgAndClassName;
        }

        public String[] getIdentifiers() {
            return identifiers;
        }

        public String getPkgAndClassName() {
            return pkgAndClassName;
        }

        public String getRootPath() {
            return rootPath;
        }
    }

    public NodeIdentifiersClassInfo createNodeIdentifiersClass(String rootPath, String defaultPkgAndClassName)
            throws IOException {
        NodeIdentifiersClassInfo result = new NodeIdentifiersClassInfo(rootPath, defaultPkgAndClassName);
        if (result.identifiers != null)
            result.generateCode();
        return result;
    }

    public void catalogNodes() {
    }

    /**
     * Given the current xmlDoc that was already read in, send it through the transformation stylesheet and assign
     * the value back to xmlDoc.
     */
    public void preProcess(File styleSheet) {
        try {
            TransformerFactory tFactory = TransformerFactory.newInstance();
            Transformer transformer = tFactory.newTransformer(new StreamSource(styleSheet));

            DOMResult result = new javax.xml.transform.dom.DOMResult();
            transformer.transform(new DOMSource(xmlDoc), result);
            xmlDoc = (Document) result.getNode();
        } catch (Exception e) {
            //StringWriter stack = new StringWriter();
            //e.printStackTrace(new PrintWriter(stack));
            addError("Failed to pre-process using style-sheet " + styleSheet.getAbsolutePath() + ": "
                    + e.toString());
        }
    }

    public Document loadXML(File file) {
        if (docSource == null) {
            errors.clear();
            sourceFiles.clear();
            metaInfoElem = null;
            metaInfoOptionsElem = null;
        }

        SourceInfo sourceInfo = new SourceInfo(docSource, file);
        sourceFiles.put(file.getAbsolutePath(), sourceInfo);

        Document doc = null;
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder parser = factory.newDocumentBuilder();
            doc = parser.parse(file);
            doc.normalize();
        } catch (Exception e) {
            throw new RuntimeException("XML Parsing error in '" + file.getAbsolutePath() + "': " + e);
        }

        if (docSource == null) {
            xmlDoc = doc;
            docSource = sourceInfo;
        }

        /*
        find all of the <include file="xyz"> elements and "include" all the
        elements in that document as children of the main document
        */

        Element rootElem = doc.getDocumentElement();
        NodeList includes = rootElem.getElementsByTagName("include");
        if (includes != null && includes.getLength() > 0) {
            for (int n = 0; n < includes.getLength(); n++) {
                Element include = (Element) includes.item(n);
                String incFileAttr = include.getAttribute("file");
                File incFile = new File(file.getParentFile(), incFileAttr);
                if (!sourceFiles.containsKey(incFile.getAbsolutePath())) {
                    Document includeDoc = loadXML(incFile);
                    if (includeDoc != null) {
                        Element includeRoot = includeDoc.getDocumentElement();
                        NodeList incChildren = includeRoot.getChildNodes();
                        for (int c = 0; c < incChildren.getLength(); c++) {
                            Node incCopy = doc.importNode(incChildren.item(c), true);
                            if (incCopy.getNodeType() == Node.ELEMENT_NODE)
                                ((Element) incCopy).setAttribute("_included-from", incFileAttr);
                            rootElem.insertBefore(incCopy, include);
                        }
                    }
                }
            }
        }

        /*
         find all of the <pre-process stylesheet="xyz"> elements and "pre-process" using XSLT stylesheets
        */

        NodeList preProcessors = rootElem.getElementsByTagName("pre-process");
        if (preProcessors != null && preProcessors.getLength() > 0) {
            for (int n = 0; n < preProcessors.getLength(); n++) {
                Element preProcessor = (Element) preProcessors.item(n);
                String ppFileAttr = preProcessor.getAttribute("style-sheet");
                if (ppFileAttr.length() == 0) {
                    addError("No style-sheet attribute provided for pre-process element");
                    continue;
                }
                File ppFile = new File(file.getParentFile(), ppFileAttr);
                docSource.addPreProcessor(new SourceInfo(docSource, ppFile));
                preProcess(ppFile);
            }
        }
        return doc;
    }

    public void saveXML(String fileName) {
        /* we use reflection so that org.apache.xml.serialize.* is not a package requirement */

        OutputStream os = null;
        try {
            Class serializerCls = Class.forName("org.apache.xml.serialize.XMLSerializer");
            Class outputFormatCls = Class.forName("org.apache.xml.serialize.OutputFormat");

            Constructor serialCons = serializerCls
                    .getDeclaredConstructor(new Class[] { OutputStream.class, outputFormatCls });
            Constructor outputCons = outputFormatCls.getDeclaredConstructor(new Class[] { Document.class });

            os = new FileOutputStream(fileName);
            Object outputFormat = outputCons.newInstance(new Object[] { xmlDoc });
            Method indenting = outputFormatCls.getMethod("setIndenting", new Class[] { boolean.class });
            indenting.invoke(outputFormat, new Object[] { new Boolean(true) });

            Object serializer = serialCons.newInstance(new Object[] { os, outputFormat });
            Method serialize = serializerCls.getMethod("serialize", new Class[] { Document.class });
            serialize.invoke(serializer, new Object[] { xmlDoc });
        } catch (Exception e) {
            throw new RuntimeException("Unable to save '" + fileName + "': " + e);
        } finally {
            try {
                if (os != null)
                    os.close();
            } catch (Exception e) {
            }
        }
    }

    public NodeList selectNodeList(String expr) throws TransformerException {
        return XPathAPI.selectNodeList(xmlDoc, expr);
    }

    public long getSelectNodeListCount(String expr) throws TransformerException {
        NodeList nodes = selectNodeList(expr);
        return nodes.getLength();
    }

    public Metric getMetrics(Metric root) {
        return null;
    }

    public String replaceExpressions(String original, Map variables) {
        if (original.indexOf("${") == -1)
            return original;

        StringBuffer sb = new StringBuffer();
        int prev = 0;

        int pos;
        while ((pos = original.indexOf("$", prev)) >= 0) {
            if (pos > 0) {
                sb.append(original.substring(prev, pos));
            }
            if (pos == (original.length() - 1)) {
                sb.append('$');
                prev = pos + 1;
            } else if (original.charAt(pos + 1) != '{') {
                sb.append(original.charAt(pos + 1));
                prev = pos + 2;
            } else {
                int endName = original.indexOf('}', pos);
                if (endName < 0) {
                    throw new RuntimeException("Syntax error in prop: " + original);
                }

                String javaExprStr = original.substring(pos + 2, endName);
                try {
                    Expression expression = ExpressionFactory.createExpression(javaExprStr);
                    JexlContext jexlContext = JexlHelper.createContext();
                    jexlContext.setVars(variables);
                    String result = expression.evaluate(jexlContext).toString();
                    sb.append(result);
                } catch (Exception e) {
                    sb.append("${" + javaExprStr + "}");
                    addError("Unable to evaluate expression '" + javaExprStr + "': " + e + ", variables: "
                            + variables.keySet());
                }

                prev = endName + 1;
            }
        }

        if (prev < original.length())
            sb.append(original.substring(prev));
        return sb.toString();
    }

    public void replaceNodeMacros(Node inNode, Set nodeNames, Map variables) {
        if (!variables.containsKey("this"))
            variables.put("this", inNode);

        NamedNodeMap attrs = inNode.getAttributes();
        if (attrs != null && attrs.getLength() > 0) {
            for (int i = 0; i < attrs.getLength(); i++) {
                Node attr = attrs.item(i);
                if (nodeNames.contains(attr.getNodeName())) {
                    String nodeValue = attr.getNodeValue();
                    String replaced = replaceExpressions(nodeValue, variables);
                    if (nodeValue != replaced)
                        attr.setNodeValue(replaced);
                }
            }
        }

        NodeList children = inNode.getChildNodes();
        for (int c = 0; c < children.getLength(); c++) {
            Node node = children.item(c);
            if (node.getNodeType() == Node.ELEMENT_NODE && nodeNames.contains(node.getNodeName())) {
                Text textNode = (Text) node.getFirstChild();
                String nodeValue = textNode.getNodeValue();
                String replaced = replaceExpressions(nodeValue, variables);
                if (nodeValue != replaced)
                    textNode.setNodeValue(replaced);
            }
        }
    }
}