/*
 * This file is part of rasdaman community.
 *
 * Rasdaman community is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Rasdaman community is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU  General Public License for more details.
 *
 * You should have received a copy of the GNU  General Public License
 * along with rasdaman community.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Copyright 2003 - 2014 Peter Baumann / rasdaman GmbH.
 *
 * For more information please see <http://www.rasdaman.org>
 * or contact Peter Baumann via <baumann@rasdaman.com>.
 */
/*
 * JOMDoc - A Java library for OMDoc documents (http://omdoc.org/jomdoc).
 *
 * Original author    Dimitar Misev <d.misev@jacobs-university.de>
 * Web                http://kwarc.info/dmisev/
 * Created            Apr 4, 2008, 5:18:39 PM
 *
 * Filename           $Id: XMLUtil.java 1976 2010-07-31 12:07:20Z dmisev $
 * Revision           $Revision: 1976 $
 * Last modified on   $Date: 2010-07-31 14:07:20 +0200 (Sat, 31 Jul 2010) $
 *               by   $Author: dmisev $
 *
 * Copyright (C) 2007,2008 the KWARC group (http://kwarc.info)
 * Licensed under the GNU  Public License v3 (GPL3).
 * For other licensing contact Michael Kohlhase <m.kohlhase@jacobs-university.de>
 */
package petascope.util;

import com.eaio.uuid.UUID;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.validation.SchemaFactory;

import nu.xom.Attribute;
import nu.xom.Builder;
import nu.xom.Comment;
import nu.xom.Document;
import nu.xom.Element;
import nu.xom.Node;
import nu.xom.Nodes;
import nu.xom.ParentNode;
import nu.xom.ParsingException;
import nu.xom.Text;
import nu.xom.XPathContext;
import nu.xom.converters.DOMConverter;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;

/**
 * Common utility methods for working with XML.
 *
 * @author <a href="mailto:d.misev@jacobs-university.de">Dimitar Misev</a>
 */
public class XMLUtil {

    private static Logger log = LoggerFactory.getLogger(XMLUtil.class);

    static class MyBuilder extends ThreadLocal<Builder> {

        boolean validating;

        public MyBuilder(boolean validating) {
            this.validating = validating;
        }

        @Override
        protected Builder initialValue() {
            factory.setValidating(validating);
            return newBuilder(!validating);
        }
    }

    /**
     * Separator, for debugging
     */
    public static String FSEP = "\n-------------------------------------\n";
    public static String HSEP = "\n-------------------\n";
    /**
     * Setup for building documents
     */
    private static SchemaFactory schemaFactory;
    private static SAXParserFactory factory;
    private static MyBuilder builder;
    private static File wcsSchema;
    public static final String XML_STD_ENCODING = "UTF-8";
    public static final String WCS_SCHEMA = "xml/ogc/wcs/2.0.0/wcsAll.xsd";
    private static final String SET_FEATURE_XXE_FALSE = "http://xml.org/sax/features/external-general-entities";


    static {
        init();
    }

    public static void init() {
        if (factory != null) {
            return;
        }
        System.setProperty("javax.xml.parsers.SAXParserFactory",
                "com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl");
        factory = SAXParserFactory.newInstance();

        try {
            factory.setFeature(SET_FEATURE_XXE_FALSE, false);
        } catch (ParserConfigurationException ex) {
            //If feature does not exist, then no XXE support anyway so there is nothing to do
            log.warn("Set feature XXE False: " + ex.getMessage());
            log.debug(ExceptionUtils.getStackTrace(ex));
        } catch (SAXNotRecognizedException ex) {
            //If feature does not exist then,no XXE support anyway so there is nothing to do
            log.warn("Set feature XXE False: " + ex.getMessage());
            log.debug(ExceptionUtils.getStackTrace(ex));
        } catch (SAXNotSupportedException ex) {
            //If feature does not exist,then no XXE support anyway so there is nothing to do
            log.warn("Set feature XXE False: " + ex.getMessage());
            log.debug(ExceptionUtils.getStackTrace(ex));
        }

        factory.setNamespaceAware(true);
        factory.setValidating(true);
        builder = new MyBuilder(false);
        builder.get();
    }

    private static Builder newBuilder(boolean ignoreDTD) {
        XMLReader xmlReader = null;
        try {
            xmlReader = factory.newSAXParser().getXMLReader();
            if (ignoreDTD) {
                xmlReader.setEntityResolver(new EntityResolver() {

                    public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
                        return new InputSource(new StringReader(""));
                    }
                });
                xmlReader.setErrorHandler(new ErrorHandler() {

                    @Override
                    public void warning(SAXParseException saxpe) throws SAXException {
                        log.warn("XML parser warning: ", saxpe.getMessage());
                    }

                    @Override
                    public void error(SAXParseException saxpe) throws SAXException {
                        throw saxpe;
                    }

                    @Override
                    public void fatalError(SAXParseException saxpe) throws SAXException {
                        throw saxpe;
                    }
                });
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return new Builder(xmlReader);
    }

    /**
     * Build XOM Document from XML file.
     *
     * @param file input XML file
     * @return XOM Document
     * @throws IOException
     * @throws ParsingException
     */
    public static Document buildDocument(File file) throws IOException, ParsingException {
        return buildDocument(file.toURI().toString(), new FileInputStream(file));
    }

    /**
     * Build XOM Document from an XML string.
     *
     * @param baseURI
     * @param document input XML string
     * @return XOM Document
     * @throws IOException
     * @throws ParsingException
     */
    public static Document buildDocument(String baseURI, String document) throws IOException, ParsingException {
        InputStream in = new ByteArrayInputStream(document.getBytes(XML_STD_ENCODING));
        return buildDocument(baseURI, in);
    }

    /**
     * Creates a {@link Document} given a input stream.
     * <p>
     * <i>Note</i>: If the input stream to parse contains a <code>DOCTYPE</code> definition, but the parser can't find the referenced <code>DTD</code>, then a
     * <code>FileNotFound</code> exception is raised discarding the parsing process.
     *
     * @param baseURI
     * @param in      an input stream
     * @return the document
     * @throws IOException
     * @throws ParsingException
     */
    public static Document buildDocument(String baseURI, InputStream in) throws IOException, ParsingException {
        Document doc = null;

        try {
            doc = builder.get().build(in, baseURI);
        } catch (ParsingException ex) {
            log.error(StringUtil.join("Error while building XML document: " + baseURI, ex.getMessage(),
                    "line: " + ex.getLineNumber() + ", column: " + ex.getColumnNumber()));
            throw ex;
        } catch (IOException ex) {
            log.error(StringUtil.join("Error while building XML document: " + baseURI,
                    "Error reading from the input stream.", ex.getMessage()));
            throw ex;
        } catch (Exception ex) {
            ex.printStackTrace();
            log.error(StringUtil.join("Error while building XML document: " + baseURI, ex.getMessage()));
            throw new RuntimeException("Error while building XML document: " + baseURI, ex);
        } finally {
            in.close();
        }

        if (baseURI != null && doc != null) {
            doc.setBaseURI(baseURI);
        }

        return doc;
    }

    /**
     * Build a collection of documents
     *
     * @param files the XML files to build
     * @return the Documents
     * @throws java.io.IOException
     * @throws IOException
     * @throws ParsingException
     */
    public static List<Document> buildDocuments(Collection<File> files) throws IOException, ParsingException {
        List<Document> ret = new ArrayList<Document>();
        for (File file : files) {
            ret.add(buildDocument(file));
        }
        return ret;
    }

    /**
     * Serialize a XOM Document.
     *
     * @param xomDocument the XOM Document to be serialized
     * @throws IOException
     */
    public static String serialize(Document xomDocument) throws IOException {
        return serialize(xomDocument, false);
    }

    /**
     * Serialize a XOM Document.
     *
     * @param xomDocument   the XOM Document to be serialized
     * @param noPrettyPrint
     * @throws IOException
     */
    public static String serialize(Document xomDocument, boolean noPrettyPrint) throws IOException {
        OutputStream os = new ByteArrayOutputStream();
        serialize(xomDocument, os, noPrettyPrint);
        return os.toString();
    }

    /**
     * Serialize a XOM Document.
     *
     * @param xomDocument the XOM Document to be serialized
     * @param os          stream where to write the result
     * @throws IOException
     */
    public static void serialize(Document xomDocument, OutputStream os) throws IOException {
        serialize(xomDocument, os, false);
    }

    /**
     * Serialize a XOM Document without pretty printing the result.
     *
     * @param xomDocument   the XOM Document to be serialized
     * @param os            stream where to write the result
     * @param noPrettyPrint
     * @throws IOException
     */
    public static void serialize(Document xomDocument, OutputStream os, boolean noPrettyPrint) throws IOException {
        nu.xom.Serializer serializer = new nu.xom.Serializer(os);
        if (!noPrettyPrint) {
            serializer.setIndent(2);
        }
        serializer.write(xomDocument);
    }

    /**
     * Serialize a XOM DOcument
     *
     * @param xomDocument the XOM Document to be serialized
     * @param file        the file to which to serialize
     * @throws IOException
     */
    public static void serialize(Document xomDocument, File file) throws IOException {
        serialize(xomDocument, file, false);
    }

    /**
     * Serialize a XOM Document without pretty printing the result.
     *
     * @param xomDocument   the XOM Document to be serialized
     * @param file
     * @param noPrettyPrint
     * @throws IOException
     */
    public static void serialize(Document xomDocument, File file, boolean noPrettyPrint) throws IOException {
        serialize(xomDocument, new FileOutputStream(file), noPrettyPrint);
    }

    /**
     * @param doc
     * @return the document name
     */
    public static String docName(Document doc) {
        if (doc == null || doc.getBaseURI() == null) {
            return null;
        }
        String ret = doc.getBaseURI();
        if (ret == null) {
            return null;
        }
        int ind = ret.lastIndexOf(File.separator);
        if (ind != -1) {
            ret = ret.substring(ind + 1);
        }
        return ret;
    }

    /**
     * @param e
     */
    public static String getBaseURI(Element e) {
        if (e == null) {
            return null;
        }
        String ret = getBaseURI(e.getDocument());
        if (ret == null) {
            ParentNode p = e;
            while (true) {
                if (p == null) {
                    return null;
                }
                p = p.getParent();
            }
        }
        return ret;
    }

    /**
     * @param doc
     */
    public static String getBaseURI(Document doc) {
        if (doc != null) {
            return doc.getBaseURI();
        }
        return null;
    }

    /**
     * @param e
     * @return the depth of <code>e</code> in the XML tree
     */
    public static int depth(Element e) {
        int ret = 0;
        ParentNode p = e.getParent();
        while (p != null) {
            p = p.getParent();
            ++ret;
        }
        return ret;
    }

    /**
     * Shortcut method for creating a XOM Attribute in the XML namespace.
     *
     * @param name  name of the attribute
     * @param value value of the attribute
     * @return the created attribute
     */
    public static Attribute createXMLAttribute(String name, String value) {
        return new Attribute(XMLSymbols.PREFIX_XML + ":" + name, XMLSymbols.NAMESPACE_XML, value);
    }

    /**
     * Deep copy one element to another.
     *
     * @param from the element to be copied
     * @param to   the element to be replaced
     */
    public static void copyElement(Element from, Element to) {
        if (from == null || to == null) {
            return;
        }

        Element e = (Element) from.copy();
        to.removeChildren();
        while (to.getAttribute(0) != null) {
            to.removeAttribute(to.getAttribute(0));
        }
        while (e.getAttribute(0) != null) {
            Attribute a = e.getAttribute(0);
            a.detach();
            to.addAttribute(a);
        }
        if (e.getChildCount() > 0) {
            while (e.getChild(0) != null) {
                Node n = e.getChild(0);
                n.detach();
                to.appendChild(n);
            }
        }
        to.setLocalName(e.getLocalName());
        to.setBaseURI(e.getBaseURI());
        to.setNamespacePrefix(e.getNamespacePrefix());
        to.setNamespaceURI(e.getNamespaceURI());
    }

    /**
     * Return new element with the same name and attributes as the given element.
     * Note that we don't care about the prefix/namespaces here.
     *
     * @param e
     * @param ignoreAttributes set of attributes to ignore when copying
     */
    public static Element copyTag(Element e, Set<String> ignoreAttributes) {
        Element ret = null;
        if (e.getNamespacePrefix() != null) {
            ret = new Element(e.getQualifiedName(), e.getNamespaceURI());
        } else {
            ret = new Element(e.getLocalName());
        }
        for (int i = 0; i < e.getAttributeCount(); i++) {
            Attribute a = (Attribute) e.getAttribute(i).copy();
            if (ignoreAttributes != null && ignoreAttributes.contains(a.getLocalName())) {
                continue;
            }
            ret.addAttribute(a);
        }
        return ret;
    }

    /**
     * cp(<label att_1="val_1"...att_n="val_n">ch</label>) --> <label att_1="val_1"...att_n="val_n">ch'</label>, where ch'!=Element
     *
     * @param n
     */
    public static Node cp(Node n) {
        if (n instanceof Element) {
            Element e = (Element) n;
            Element ne = copyTag(e, null);
            if (firstChild(e) == null) {
                String v = e.getValue();
                if (!"".equals(v)) {
                    ne.appendChild(v);
                }
            }
            return ne;
        }

        if (!(n instanceof Text)) {
            n.detach();
            return n.copy();
        } else {
            return (Text) n.copy();
        }
    }

    /**
     * @param xml
     * @param tag
     */
    public static boolean isFirstTag(String xml, String tag) {
        return tag.equals(getRootElementName(xml));
    }

    /**
     * @param xml
     */
    public static String getRootElementName(String xml) {
        int start = 0;
        while (start < xml.length()) {
            start = xml.indexOf("<", start);
            if (start == -1) {
                return null;
            }
            if (xml.charAt(start + 1) != '?') {
                int end = start + 1;
                while (end < xml.length() && xml.charAt(end) != ' ' && xml.charAt(end) != '>') {
                    if (xml.charAt(end) == ':') {
                        start = end;
                    }
                    ++end;
                }
                if (end == -1) {
                    return null;
                }
                ++start;
                return xml.substring(start, end).trim();
            } else {
                ++start;
            }
        }
        return null;
    }

    /**
     * @param xml
     */
    public static String removeXmlDecl(String xml) {
        if (xml.startsWith("<?xml")) {
            xml = xml.substring(xml.indexOf("<", 1));
        }
        return xml;
    }

    /**
     * Return the text that some node contains.
     *
     * @param node
     */
    public static String getText(Element node) {
        if (node == null) {
            return null;
        }
        StringBuilder ret = new StringBuilder();
        for (int i = 0; i < node.getChildCount(); i++) {
            ret.append(node.getChild(i).toXML());
        }
        return ret.toString().trim();
    }

    public static String getXMLID(Element e) {
        if (e == null) {
            return null;
        }
        String ret = e.getAttributeValue("id", XMLSymbols.NAMESPACE_XML);
        if (ret == null) {
            ret = e.getAttributeValue("id");
        }
        return ret;
    }

    public static String getXMLIDOrName(Element e) {
        if (e == null) {
            return null;
        }
        String ret = getXMLID(e);
        if (ret == null) {
            ret = e.getAttributeValue("name");
        }
        return ret;
    }

    /**
     * Collect all children of <code>e</code> with the given names.
     *
     * @return a list of the collected elements
     */
    public static List<Element> collectAll(Element e, String... names) {
        return collectAllExcept(e, null, true, names);
    }

    public static List<Element> collectAllExcept(Element e, String except, boolean recurse, String... names) {
        List<Element> ret = new ArrayList<Element>();
        Set<String> namesSet = new HashSet<String>(Arrays.asList(names));
        collectAllExcept(ret, e, except, recurse, namesSet);
        return ret;
    }

    private static void collectAllExcept(List<Element> ret, Element e, String except, boolean recurse, Set<String> names) {
        String elname = e.getLocalName();
        if (names.contains(elname)) {
            ret.add(e);
            if (!recurse) {
                return;
            }
        }
        int n = e.getChildCount();
        for (int i = 0; i < n; i++) {
            Node node = e.getChild(i);
            if (node instanceof Element && !((Element) node).getLocalName().equals(except)) {
                collectAllExcept(ret, (Element) node, except, recurse, names);
            }
        }
    }

    /**
     * Collect all children of <code>e</code> with the given name.
     *
     * @return a list of the collected elements
     */
    public static List<Element> collectAll(Element e, String prefix, String name, XPathContext ctx) {
        List<Element> ret = new ArrayList<Element>();
        Nodes notations = null;
        if (ctx != null) {
            if (prefix != null) {
                notations = e.query("//" + prefix + ":" + name, ctx);
            } else {
                notations = e.query("//" + name, ctx);
            }
        }
        if (notations != null) {
            for (int i = 0; i < notations.size(); i++) {
                ret.add((Element) notations.get(i));
            }
        }
        return ret;
    }

    /**
     * @return the first child of <code>e</code> with the given <code>id</code>
     * when searched with DFS.
     */
    public static Element childWithId(Element e, String id) {
        String value = getXMLIDOrName(e);
        if (value != null && value.equals(id)) {
            return e;
        } else {
            int n = e.getChildCount();
            Element ret = null;
            for (int i = 0; i < n; i++) {
                Node c = e.getChild(i);
                if (c instanceof Element) {
                    ret = childWithId((Element) c, id);
                    if (ret != null) {
                        return ret;
                    }
                }
            }
            return ret;
        }
    }

    /**
     * Replaces an <code>o</code> with a new child node. If <code>o</code> does not have a parent node, then a <code>NoSuchChildException</code> is thrown.
     *
     * @param <T> the type parameter to specify the node type
     * @param o   the old node
     * @param n   the new node
     * @return the new node
     */
    public static <T extends Node> T substitute(T o, T n) {
        ParentNode p = o.getParent();
        if (p == null) {
            return n;
        }
        if (n != null) {
            n.detach();
            p.replaceChild(o, n);
        } else {
            p.removeChild(o);
        }
        return n;
    }

    /**
     * Remove all qualifiers in an XML code.
     *
     * @param xml the XML document
     * @return the same document with unqualified element names
     */
    public static String removeQualifiers(String xml) {
        // remove from elements
        xml = xml.replaceAll("<[^/:> ]+:", "<").replaceAll("</[^:> ]+:", "</");
        // remove namespace declarations
        xml = xml.replaceAll("xmlns:\\w+ *= *\"[^\"]*\"", "");
        // remove from attributes
        xml = xml.replaceAll("\" *\\w+ *: *(\\w+ *= *\")", "\" $1");
        xml = xml.replaceAll("(<\\w+ +)\\w+ *: *(\\w+ *= *\")", "$1$2");
        return xml;
    }

    /**
     * Remove all attributes from the given <code>xml</code> string
     */
    public static String removeAttributes(String xml, String[] attributes) {
        for (String att : attributes) {
            xml = xml.replaceAll(" *(\\w+:)?" + att + " *= *\"[^\"]*\" *", " ");
        }
        return xml;
    }

    /**
     * Wrap the given list of nodes in a root element
     *
     * @param n         nodes to wrap
     * @param copy      whether to append copies of the nodes. If false then the given nodes are detached!
     * @param prefix    root element prefix
     * @param name      root element name
     * @param namespace root element namespace
     * @return the root element
     */
    public static <T extends Node> Element wrap(boolean copy, String prefix, String name, String namespace, T... n) {
        Element ret = new Element(prefix + ":" + name, namespace);
        if (copy) {
            Node x = null;
            for (Node node : n) {
                x = node.copy();
                x.detach();
                ret.appendChild(node);
            }
        } else {
            for (Node node : n) {
                node.detach();
                ret.appendChild(node);
            }
        }
        return ret;
    }

    /**
     * Retrieve a type 4 (pseudo randomly generated) UUID.
     *
     * @return a randomly generated UUID.
     */
    public static String newUUID() {
        return "a" + (new UUID());
    }

    /**
     * Get the first child <code>Element</code> of <code>e</code>
     */
    public static Element firstChild(Element e) {
        return firstChild(e, null);
    }

    public static Element firstChild(Element e, String name) {
        Node n = null;
        for (int i = 0; i < e.getChildCount(); i++) {
            n = e.getChild(i);
            if (n instanceof Element) {
                if (name == null || ((Element) n).getLocalName().equals(name)) {
                    return (Element) n;
                }
            }
        }
        return null;
    }

    /**
     * Returns first child element whose name matches a certain pattern.
     *
     * @param e       Root element
     * @param pattern The pattern to match
     * @return The first child element which matches the pattern (if it exists)
     */
    public static Element firstChildPattern(Element e, String pattern) {
        Node n = null;
        for (int i = 0; i < e.getChildCount(); i++) {
            n = e.getChild(i);
            if (n instanceof Element) {
                if (pattern == null || ((Element) n).getLocalName().matches(pattern)) {
                    return (Element) n;
                }
            }
        }
        return null;
    }

    public static Element firstChildRecursive(Element e, String name) {
        Node n = null;
        for (int i = 0; i < e.getChildCount(); i++) {
            n = e.getChild(i);
            if (n instanceof Element) {
                if (name == null || ((Element) n).getLocalName().equals(name)) {
                    return (Element) n;
                }
                Element ret = firstChildRecursive((Element) n, name);
                if (ret != null) {
                    return ret;
                }
            }
        }
        return null;
    }

    /**
     * Returns first child element whose name matches a certain pattern, recursively through the XML nodes.
     *
     * @param e       Root element
     * @param pattern The pattern to match
     * @return The first child element which matches the pattern (if it exists)
     */
    public static Element firstChildRecursivePattern(Element e, String pattern) {
        Node n = null;
        for (int i = 0; i < e.getChildCount(); i++) {
            n = e.getChild(i);
            if (n instanceof Element) {
                if (pattern == null || ((Element) n).getLocalName().matches(pattern)) {
                    return (Element) n;
                }
                Element ret = firstChildRecursivePattern((Element) n, pattern);
                if (ret != null) {
                    return ret;
                }
            }
        }
        return null;
    }

    /**
     * ch (e) --> [c_1,...,c_n], where parent(c_i)=e for all c_i
     */
    public static List<Node> ch(Node n) {
        List<Node> ret = new ArrayList<Node>();
        for (int i = 0; i < n.getChildCount(); i++) {
            Node c = n.getChild(i);

            if (c instanceof Text) {
                if (!"".equals(c.toXML().trim())) {
                    ret.add(c);
                }
            } else if (!(c instanceof Comment)) {
                ret.add(c);
            }
        }
        return ret;
    }

    /**
     * ch (e, n) --> [c_1,...,c_n], where parent(c_i)=e and name(c_i)=n for all c_i
     */
    public static List<Node> ch(Node n, String name) {
        if (n instanceof Element) {
            return ListUtil.<Node, Element>cast(ch((Element) n, name));
        } else {
            return Collections.<Node>emptyList();
        }
    }

    /**
     * ch (e) --> [c_1,...,c_n], where parent(c_i)=e for all c_i
     */
    public static List<Element> ch(Element e) {
        return ch(e, (String) null);
    }

    /**
     * ch (e, n) --> [c_1,...,c_n], where parent(c_i)=e and name(c_i)=n for all c_i
     */
    public static List<Element> ch(Element e, String name) {
        List<Element> l = new LinkedList<Element>();
        Element el = null;
        for (int i = 0; i < e.getChildCount(); i++) {
            if (e.getChild(i) instanceof Element) {
                el = (Element) e.getChild(i);
                if (name == null || el.getLocalName().equals(name)) {
                    l.add(el);
                }
            }
        }
        return l;
    }

    /**
     * ch (e, n) --> [c_1,...,c_n], where parent(c_i)=e and name(c_i)=n for all c_i
     */
    public static List<Element> children(Element e, String... names) {
        List<Element> l = new LinkedList<Element>();
        Element el = null;
        for (int i = 0; i < e.getChildCount(); i++) {
            if (e.getChild(i) instanceof Element) {
                el = (Element) e.getChild(i);
                if (names != null) {
                    String tmp = el.getLocalName();
                    for (String n : names) {
                        if (n.equals(tmp)) {
                            l.add(el);
                            break;
                        }
                    }
                } else {
                    l.add(el);
                }
            }
        }
        return l;
    }

    /**
     * ch (e) --> [c_1,...,c_n], where parent(c_i)=e for all c_i
     */
    public static List<Node> elch(Node n) {
        List<Node> ret = new ArrayList<Node>();
        for (int i = 0; i < n.getChildCount(); i++) {
            Node c = n.getChild(i);
            if (c instanceof Element) {
                ret.add(c);
            }
        }
        return ret;
    }

    /**
     * Extract elements.
     * <p>
     * chex (e, n) --> [c_1,...,c_n], where parent(c_i)=e and name(c_i)!=n for all c_i
     */
    public static List<Node> chex(Node n, String name) {
        if (n instanceof Element) {
            Node child = null;
            Element e = null;
            List<Node> l = new LinkedList<Node>();
            for (int i = 0; i < n.getChildCount(); i++) {
                child = n.getChild(i);
                if (child instanceof Element) {
                    e = (Element) child;
                    if (!e.getLocalName().equals(name)) {
                        l.add(child);
                    }
                }
            }
            return l;
        } else {
            return Collections.<Node>emptyList();
        }
    }

    /**
     * Get the first parent element <code>name</code> of <code>el</code>
     *
     * @param el    this element
     * @param names the name of the parent element
     * @return the parent element
     */
    public static Element getParent(Node el, String... names) {
        ParentNode p = el.getParent();
        while (p != null) {
            if (p instanceof Element) {
                Element e = (Element) p;
                String name = e.getLocalName();
                for (String s : names) {
                    if (name.equals(s)) {
                        return e;
                    }
                }
            }
            p = p.getParent();
        }
        return null;
    }

    /**
     * Get the first parent element <code>name</code> of <code>el</code>
     *
     * @param el   this element
     * @param name the name of the parent element
     * @return the parent element
     */
    public static Element getParent(Element el, String name) {
        ParentNode p = el;
        while (p != null) {
            if (p instanceof Element) {
                Element e = (Element) p;
                if (e.getLocalName().equals(name)) {
                    return e;
                }
            }
            p = p.getParent();
        }
        return null;
    }

    public static org.w3c.dom.Document convert(Document n) {
        DocumentBuilder db = null;
        try {
            db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            return DOMConverter.convert(n, db.getDOMImplementation());
        } catch (ParserConfigurationException exc) {
            System.err.println(exc.getMessage());
            return null;
        }
    }

    /**
     * Converts a DOM NodeList to a list of XOM nodes
     *
     * @param l the DOM NodeList
     * @return a list of XOM nodes
     */
    /* (cl) */
    public static List<Node> convert(org.w3c.dom.NodeList l) {
        List<Node> result = new ArrayList<Node>();
        for (int index = 0; index < l.getLength(); index++) {
            org.w3c.dom.Node n = l.item(index);
            if (n.getNodeType() != org.w3c.dom.Node.ELEMENT_NODE) {
                continue;
            }
            result.add(DOMConverter.convert((org.w3c.dom.Element) l.item(index)));
        }
        return result;
    }

    /**
     * Converts a list of XOM nodes to an equivalent DOM NodeList
     *
     * @param l the list of XOM nodes
     * @return a DOM NodeList equivalent to l
     */
    /* (cl) */
    public static org.w3c.dom.NodeList convert(List<Node> l) {
        Element dummyRoot = new Element("DummyRoot");
        for (Node n : l) {
            dummyRoot.appendChild(n);
        }
        Document dummyDoc = new Document(dummyRoot);

        DocumentBuilder db = null;
        try {
            db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            final org.w3c.dom.Document doc = DOMConverter.convert(dummyDoc, db.getDOMImplementation());

            /*
             * TODO test this again once we use a newer version of Saxon For some strange reason, this does not work if convert is called by a Saxon XSLT extension function. If we return
             * this as instance of something that's more powerful than NodeList, Saxon 8.9 recognizes that and starts output at the DummyRoot element.
             */
            // return doc.getDocumentElement().chn();
            return new org.w3c.dom.NodeList() {

                public int getLength() {
                    return doc.getDocumentElement().getChildNodes().getLength();
                }

                public org.w3c.dom.Node item(int index) {
                    return doc.getDocumentElement().getChildNodes().item(index);
                }
            };
        } catch (ParserConfigurationException exc) {
            log.error("Error converting", exc);
            return null;
        }
    }

    /**
     * Collect all ids in <code>e</code>
     *
     * @param e the given element
     * @return a set of all ids
     */
    public static Set<String> collectIds(Element e) {
        Set<String> ret = new HashSet<String>();
        Nodes res = e.query("//@xml:id", XMLSymbols.CTX_XML);
        for (int i = 0; i < res.size(); i++) {
            Node n = res.get(i);
            if (n instanceof Attribute) {
                ret.add(((Attribute) n).getValue());
            }
        }
        return ret;
    }

    /**
     * Append an XML fragment to an XOM element.
     *
     * @param fragment
     * @return Element of the fragment, with no parents.
     * @throws IOException
     * @throws ParsingException
     */
    public static Element parseXmlFragment(String fragment) throws IOException, ParsingException {
        Builder docBuilder = new Builder();
        Element fragmentNode = docBuilder.build(new StringReader(fragment)).getRootElement();
        return (Element) fragmentNode.copy();
    }
}
