/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jackrabbit.test.api;

import org.apache.jackrabbit.test.AbstractJCRTest;
import org.apache.jackrabbit.test.XMLChar;

import org.xml.sax.SAXException;
import org.xml.sax.ContentHandler;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Attr;
import org.w3c.dom.NamedNodeMap;

import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.Transformer;
import javax.jcr.Session;
import javax.jcr.Workspace;
import javax.jcr.NamespaceRegistry;
import javax.jcr.RepositoryException;
import javax.jcr.Item;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Property;
import javax.jcr.PropertyType;
import javax.jcr.PropertyIterator;
import javax.jcr.Value;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.ArrayList;
import java.io.File;
import java.io.IOException;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.ByteArrayOutputStream;
import java.io.StringWriter;

/**
 * <code>ExportDocViewTest</code> tests the two Session methods :
 * {@link Session#exportDocumentView(String, ContentHandler, boolean, boolean)}
 * and {@link Session#exportDocumentView(String, java.io.OutputStream, boolean, boolean)}
 * against the required behaviours according the document view xml mapping
 * defined in the JSR 170 specification in chapter 6.4.2, 6.4.3 and 6.4.4 .
 *
 */
public class ExportDocViewTest extends AbstractJCRTest {

    private final boolean CONTENTHANDLER = true, STREAM = false;
    private final boolean SKIPBINARY = true, SAVEBINARY = false;
    private final boolean NORECURSE = true, RECURSE = false;

    /**
     * Resolved Name for jcr:xmltext
     */
    private String JCR_XMLTEXT;
    /**
     * Resolved Name for jcr:xmlcharacters
     */
    private String JCR_XMLDATA;
    /**
     * the stack of the text node values to check
     */
    private Stack<StackEntry> textValuesStack;

    private class StackEntry {
        // the list of text node values of the text nodes of an xml element
        List<String> textValues;
        // the current position in the ArrayList
        int position = 0;
    }

    /**
     * indicates if the tested repository exports multivalued properties.
     */
    private boolean exportMultivalProps = false;

    /**
     * indicates if the tested repository escapes (xml)invalid jcr names.
     */
    private boolean exportInvalidXmlNames = false;

    private boolean skipBinary;
    private boolean noRecurse;
    private boolean withHandler;

    private File file;
    private Session session;
    private Workspace workspace;
    private NamespaceRegistry nsr;
    private String testPath;

    private Document doc;

    protected void setUp() throws Exception {
        isReadOnly = true;
        session = getHelper().getReadOnlySession();
        workspace = session.getWorkspace();
        nsr = workspace.getNamespaceRegistry();
        file = File.createTempFile("docViewExportTest", ".xml");
        super.setUp();

        JCR_XMLTEXT = session.getNamespacePrefix(NS_JCR_URI) + ":xmltext";
        JCR_XMLDATA = session.getNamespacePrefix(NS_JCR_URI) + ":xmlcharacters";

        testPath = testRoot;
    }

    protected void tearDown() throws Exception {
        file.delete();
        if (session != null) {
            session.logout();
            session = null;
        }
        workspace = null;
        nsr = null;
        super.tearDown();
    }

    public void testExportDocView_handler_session_skipBinary_noRecurse()
            throws IOException, RepositoryException, SAXException, TransformerException {
        doTestExportDocView(CONTENTHANDLER, SKIPBINARY, NORECURSE);
    }

    public void testExportDocView_handler_session_skipBinary_recurse()
            throws IOException, RepositoryException, SAXException, TransformerException {
        doTestExportDocView(CONTENTHANDLER, SKIPBINARY, RECURSE);
    }

    public void testExportDocView_handler_session_saveBinary_noRecurse()
            throws IOException, RepositoryException, SAXException, TransformerException {
        doTestExportDocView(CONTENTHANDLER, SAVEBINARY, NORECURSE);
    }

    public void testExportDocView_handler_session_saveBinary_recurse()
            throws IOException, RepositoryException, SAXException, TransformerException {
        doTestExportDocView(CONTENTHANDLER, SAVEBINARY, RECURSE);
    }

    public void testExportDocView_stream_session_skipBinary_recurse()
            throws IOException, RepositoryException, SAXException, TransformerException {
        doTestExportDocView(STREAM, SKIPBINARY, RECURSE);
    }

    public void testExportDocView_stream_session_skipBinary_noRecurse()
            throws IOException, RepositoryException, SAXException, TransformerException {
        doTestExportDocView(STREAM, SKIPBINARY, NORECURSE);
    }

    public void testExportDocView_stream_session_saveBinary_noRecurse()
            throws IOException, RepositoryException, SAXException, TransformerException {
        doTestExportDocView(STREAM, SAVEBINARY, NORECURSE);
    }

    public void testExportDocView_stream_session_saveBinary_recurse()
            throws IOException, RepositoryException, SAXException, TransformerException {
        doTestExportDocView(STREAM, SAVEBINARY, RECURSE);
    }

    /**
     * Tests session.exportDocView with the different argument possibilities.
     * The flag withHandler decides if the method requiring a ContentHandler as
     * argument is called. The class org.apache.xml.serialize.XMLSerializer is
     * taken as ContentHandler in this case. In both cases ( export with a
     * ContentHandler and export with Stream) the test node is exported to the
     * file defined in the setUp. This exported file is parsed using
     * javax.xml.transform package and the receiving document is compared with
     * the test node and its properties and child nodes in the repository.
     *
     * @param withHandler boolean, decides to call method requiring a
     *                    ContentHandler as argument
     * @param skipBinary
     * @param noRecurse
     */
    public void doTestExportDocView(boolean withHandler, boolean skipBinary, boolean noRecurse)
            throws RepositoryException, IOException, SAXException, TransformerException {

        this.skipBinary = skipBinary;
        this.noRecurse = noRecurse;
        this.withHandler = withHandler;
        BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(file));
        try {
            if (withHandler) {
                SAXTransformerFactory stf =
                    (SAXTransformerFactory) SAXTransformerFactory.newInstance();
                TransformerHandler th = stf.newTransformerHandler();
                th.setResult(new StreamResult(os));
                session.exportDocumentView(testPath, th, skipBinary, noRecurse);
            } else {
                session.exportDocumentView(testPath, os, skipBinary, noRecurse);
            }
        } finally {
            os.close();
        }

        // build the DOM tree
        InputStream in = new BufferedInputStream(new FileInputStream(file));
        doc = readDocument(in);

        compareTree();
    }

    /**
     * Compares the test node with the document's root element. In case also the
     * child nodes are exported (noRecurse = false) the child nodes of the test
     * node are compared with the child elements of the root element too.
     *
     * @throws RepositoryException
     */
    private void compareTree() throws RepositoryException, IOException {
        Element root = doc.getDocumentElement();
        textValuesStack = new Stack<StackEntry>();
        // we assume the path is valid
        Item item = session.getItem(testPath);

        // only an absolute path to a node is allowed
        if (!item.isNode()) {
            fail("Item at the given root path " + testPath + " is not a node.");
        }
        Node node = (Node) item;
        // multival props exported?
        setExportMultivalProps(node, root, false);
        // items with invalid xml names exported?
        setExportInvalidXmlNames(node, root, false);
        // check the test root node
        checkRootElement(node, root);
        // check the namespaces
        compareNamespaces(root);
        // check the exported data against the node which is exported.
        compareNode(node, root);

        // check the whole tree
        if (!noRecurse) {
            checkChildNodes(node, root);
        }
    }

    /**
     * Assures that root element exists and has correct jcr:root name if it is
     * the root node of the repository. (chapter 6.4.2.2 of the JCR
     * specification.) Also checks if multivalued properties are exported
     * (chapter 6.4.2.5 of the JCR specification.) Also tries to find out if
     * items with an invalid xml name are exported or not. (chapter 6.4.2.4 of
     * the JCR specification.)
     *
     * @param node
     * @param root
     * @throws RepositoryException
     */
    private void checkRootElement(Node node, Element root) throws RepositoryException {

        boolean isValidName = XMLChar.isValidName(node.getName());
        if (root != null) {
            // check correct element name if the root node of the repository is exported.
            if (node.getDepth() == 0) {
                assertEquals("Exported root node has not correct name jcr:root.",
                        "jcr:root", root.getTagName());
            }
        } else {
            if (exportInvalidXmlNames || isValidName) {
                fail("Node " + node.getPath() + " is not exported.");
            }
        }
    }

    /**
     * Checks the child nodes of the given node against the child nodes of the
     * given xml element. The found text nodes of the xml element are hold in an
     * ArrayList and put on a stack for further checking if another child
     * element is between them.
     *
     * @param node
     * @param elem
     * @throws RepositoryException
     */
    private void checkChildNodes(Node node, Element elem)
            throws RepositoryException, IOException {

        NodeIterator nodeIter = node.getNodes();
        if (getSize(node.getNodes()) == 0) {
            assertTrue("Exported node " + node.getPath() + " has child elements " +
                    "although it has no child nodes ", 0 == countChildElems(elem));
        } else {
            // create a stack entry for the text child nodes
            // of the current xml element
            StackEntry entry = new StackEntry();
            entry.textValues = getChildTextNodeValues(elem);
            textValuesStack.push(entry);
            // xmltext nodes directly following each other
            // are serialized together as xml text
            List<Node> jcrTextNodes = new ArrayList<Node>();

            while (nodeIter.hasNext()) {
                Node childNode = nodeIter.nextNode();
                if (isXMLTextNode(childNode)) {
                    jcrTextNodes.add(childNode);
                } else {
                    if (jcrTextNodes.size() > 0) {
                        compareXmltextNodes(jcrTextNodes, elem);
                        // reset the Array
                        jcrTextNodes.clear();
                    }
                    compareChildTree(childNode, elem);
                }
            }
            // finally we are through the child nodes
            // so we delete the stackEntry
            textValuesStack.pop();
        }
    }

    /**
     * Compares the child tree of a given node against the child elements of a
     * given element. (chapter 6.4.2.1 points 2,3,4 of the JCR specification).
     * <p>
     * Considered are the export constraints regarding nodes named jcr:xmldata
     * (chapter 6.4.2.3 of the JCR specification).
     * <p>
     * Also the numbers of exported child elements is compared with the number
     * of child nodes.
     *
     * @param node
     * @param parentElem
     * @throws RepositoryException
     */
    private void compareChildTree(Node node, Element parentElem)
            throws RepositoryException, IOException {

        Element nodeElem;

        // find a childElem belonging to the node and check it.
        nodeElem = findElem(node, parentElem);
        if (nodeElem != null) {
            compareNode(node, nodeElem);
            // go deep
            checkChildNodes(node, nodeElem);
        }
    }

    /**
     * Checks the given Element if it has a child element with the same (or
     * eventually escaped) name as the given node. (chapter 6.4.2.1 point 3 of
     * the JCR specification).
     *
     * @param node
     * @param parentElem
     * @return Child Element of parentElem. Null if no corresponidng element is
     *         found.
     * @throws RepositoryException
     */
    private Element findElem(Node node, Element parentElem) throws RepositoryException {
        String name = node.getName();
        Element nodeElem = null;
        // valid xml name?
        boolean isValidName = XMLChar.isValidName(name);

        name = !isValidName ? escapeNames(name) : name;
        // same name sibs
        List<Element> children = getChildElems(parentElem, name);

        if (children.size() > 0) {
            // xmltext nodes are not exported as elements
            if (isXMLTextNode(node)) {
                fail("Xml text node " + node.getPath() +
                        " is wronlgy exported as element.");
            } else {
                // order of same name siblings is preserved during export
                int index = node.getIndex();
                try {
                    nodeElem = children.get(index - 1);
                } catch (IndexOutOfBoundsException iobe) {
                    fail("Node " + node.getPath() + " is not exported."
                            + iobe.toString());
                }
            }
        } else {
            // need to be exported
            if (!isXMLTextNode(node) && (isValidName || exportInvalidXmlNames)) {
                fail("Node " + node.getPath() + " is not exported.");
            }
        }
        return nodeElem;
    }

    /**
     * Check if a property of a node is exported. This is true if a
     * corresponding attribute is found in the element the node is exported to.
     * An attribute is corresponding when it has the same name as the given
     * property (or it is equal to its escaped name). (chapter 6.4.2.1 point 5
     * of the JCR specification).
     *
     * @param prop
     * @param elem
     * @return
     * @throws RepositoryException
     */
    private Attr findAttribute(Property prop, Element elem)
            throws RepositoryException {

        String name = prop.getName();

        boolean isValidName = XMLChar.isValidName(name);

        name = !isValidName ? escapeNames(name) : name;
        Attr attribute = elem.getAttributeNode(name);
        return attribute;
    }

    /**
     * Check if a property should be exported according the three choices
     * skipBinary, exportMultivalProps and exportInvalidXmlNames.
     *
     * @param prop
     * @param attribute
     * @throws RepositoryException
     */
    private void checkAttribute(Property prop, Attr attribute) throws RepositoryException {

        boolean isBinary = (prop.getType() == PropertyType.BINARY);
        boolean isMultiple = prop.getDefinition().isMultiple();
        if (skipBinary) {
            if (isBinary && !(isMultiple && !exportMultivalProps)) {
                assertEquals("Value of binary property " + prop.getPath() +
                        " exported although skipBinary is true",
                        attribute.getValue().length(), 0);
            }
            // check the flags
            else {
                checkExportFlags(prop, attribute);
            }
        }
        // saveBinary
        else {
            if (isBinary && !(isMultiple && !exportMultivalProps)) {
                assertTrue("Binary property " + prop.getPath() +
                        " not exported although skipBinary is false", attribute != null);
            }
            // check anyway the flags
            checkExportFlags(prop, attribute);
        }
    }

    /**
     * Checks attribute export regarding the two flags and without considering
     * skipBinary.
     *
     * @param prop
     * @param attribute
     * @throws RepositoryException
     */
    private void checkExportFlags(Property prop, Attr attribute)
            throws RepositoryException {

        String name = prop.getName();
        boolean isMultiple = prop.getDefinition().isMultiple();
        boolean isValidName = XMLChar.isValidName(name);

        if (isMultiple) {
            if (exportMultivalProps) {
                assertTrue("Not all multivalued properties are exported: "
                        + prop.getPath() + " is not exported.", attribute != null);
            } else {
                // skipping multi-valued properties entirely is legal
                // according to "6.4.2.5 Multi-value Properties" of the
                // jsr-170 specification
                return;
            }
        }
        // check anyway the other flag
        if (exportInvalidXmlNames && !isValidName) {
            assertTrue("Not all properties with invalid xml name are exported: " +
                    prop.getPath() + " is not exported.", attribute != null);
        } else {
            assertTrue("Property " + prop.getPath() + " is not exported.",
                    attribute != null);
        }
    }

    /**
     * Compares the given node with the given element. Comparison is succesful
     * if the number of exported child nodes and exported properties match the
     * found child elements and attributes considering the possible exceptions
     * and if the comparison of the properties of the node with the attributes
     * of the element is successful too.
     *
     * @param node
     * @param elem
     * @throws RepositoryException
     */
    private void compareNode(Node node, Element elem)
            throws RepositoryException, IOException {
        // count the child nodes and compare with the exported child elements
        compareChildNumber(node, elem);
        // count the properties and compare with attributes exported
        comparePropNumber(node, elem);

        PropertyIterator propIter = node.getProperties();
        while (propIter.hasNext()) {
            Property prop = propIter.nextProperty();
            Attr attr = findAttribute(prop, elem);
            checkAttribute(prop, attr);
            if (attr != null) {
                compareProperty(prop, attr);
            }
        }
    }

    /**
     * Compare the given property with the given attribute. Comparison is
     * successful if their values can be matched considering binary type,
     * multivalue export. (chapter 6.4.2.1 point 6 of the JCR specification).
     *
     * @param prop
     * @param attr
     * @throws RepositoryException
     */
    private void compareProperty(Property prop, Attr attr)
            throws RepositoryException, IOException {

        boolean isMultiple = prop.getDefinition().isMultiple();
        boolean isBinary = (prop.getType() == PropertyType.BINARY);
        String attrVal = attr.getValue();
        String val = null;
        if (isMultiple) {
            val = exportValues(prop, isBinary);
        } else {
            if (isBinary) {
                try {
                    attrVal = decodeBase64(attrVal);
                    val = prop.getString();
                } catch (IOException ioe) {
                    fail("Could not decode value of binary attribute " +
                            attr.getName() + " of element " +
                            attr.getOwnerElement().getTagName());
                }
            } else {
                val = prop.getString();
            }
        }
        if (isBinary && skipBinary) {
            assertEquals("Value of binary property " + prop.getPath() +
                    " is not exported correctly: ", "", attrVal);
            assertEquals("Value of binary property " + prop.getPath() +
                    " exported although skipBinary is true",
                    "", attrVal);
        } else {
            assertTrue("Value of property " + prop.getPath() +
                    " is not exported correctly: " + attrVal,
                    val.equals(attrVal) || escapeValues(val).equals(attrVal));
        }
    }

    /**
     * Checks if all registered namespaces are exported into the root element.
     * (chapter 6.4.2.1 point 1 of the JCR specification).
     *
     * @param root
     * @throws RepositoryException
     */
    private void compareNamespaces(Element root) throws RepositoryException {

        Map<String, String> nameSpaces = new AttributeSeparator(root).getNsAttrs();
        // check if all namespaces exist that were exported
        for (Iterator<String> e = nameSpaces.keySet().iterator(); e.hasNext(); ) {
            String prefix = e.next();
            String URI = nameSpaces.get(prefix);

            assertEquals("Prefix of uri" + URI + "is not exported correctly.",
                    nsr.getPrefix(URI), prefix);
            assertEquals("Uri of prefix " + prefix + "is not exported correctly.",
                    nsr.getURI(prefix), URI);
        }

        String[] registeredNamespaces = nsr.getURIs();
        // check if all required namespaces are exported
        for (int i = 0; i < registeredNamespaces.length; i++) {
            String prefix = nsr.getPrefix(registeredNamespaces[i]);
            // skip default namespace and xml namespaces
            if (prefix.length() == 0 || prefix.startsWith("xml")) {
                continue;
            } else {
                assertTrue("namespace: " + registeredNamespaces[i] + " not exported", nameSpaces.keySet().contains(prefix));
            }
        }
    }

    /**
     * Count the number of child nodes of a node which are exported and compare
     * with the number expected.
     *
     * @param node
     * @param elem
     * @throws RepositoryException
     */
    private void compareChildNumber(Node node, Element elem) throws RepositoryException {
        NodeIterator iter = node.getNodes();
        long size = 0;

        long exported = countChildElems(elem);
        // child tree is exported too
        if (!noRecurse) {
            size = getSize(node.getNodes());
            while (iter.hasNext()) {
                Node n = iter.nextNode();
                String name = n.getName();

                // xmltext node ?
                if (isXMLTextNode(n)) {
                    size--;
                }
                if (!exportInvalidXmlNames && !XMLChar.isValidName(name)) {
                    size--;
                }

            }
        }
        assertEquals("The number of child nodes of node  " + node.getPath() +
                " which are exported is not correct: ", size, exported);
    }

    /**
     * Count the number of exported properties of a given node and compare with
     * the number of the properties expected to be exported.
     *
     * @param node
     * @param elem
     * @throws RepositoryException
     */
    private void comparePropNumber(Node node, Element elem)
            throws RepositoryException {

        PropertyIterator iter = node.getProperties();
        long size = getSize(node.getProperties());
        long exported = new AttributeSeparator(elem).getNonNsAttrs().size();
        while (iter.hasNext()) {
            Property prop = iter.nextProperty();
            String name = prop.getName();
            boolean isMultiple = prop.getDefinition().isMultiple();

            // props not exported so we decrease the expected size.
            if (!exportInvalidXmlNames && !XMLChar.isValidName(name)) {
                size--;
            } else if (!exportMultivalProps && isMultiple) {
                size--;
            }
        }
        assertEquals("The number of properties exported of node " + node.getPath() +
                " is not correct.", size, exported);
    }

    /**
     * Compares the text of a given XML element with the values of the
     * jcr:xmlcharacters properties of the given jcr:xmltext nodes sequel. If
     * the sequel has more than one node the serialized values are concatenated
     * with a space. We only check the case withHandler is true.
     *
     * @param nodes
     * @param parentElem
     * @throws RepositoryException
     */
    private void compareXmltextNodes(List<Node> nodes, Element parentElem)
            throws RepositoryException {
        // only this case
        if (withHandler) {
            String value = "";
            String exportedVal = "";

            StackEntry currentEntry = (StackEntry) textValuesStack.pop();
            try {
                exportedVal = (String) currentEntry.textValues.get(currentEntry.position);
                currentEntry.position++;
                textValuesStack.push(currentEntry);
            } catch (IndexOutOfBoundsException iobe) {
                fail("Xmltext nodes not correctly exported: " + iobe.getMessage());
            }

            int size = nodes.size();
            if (size == 1) {
                Node node = nodes.get(0);
                Property prop = node.getProperty(JCR_XMLDATA);
                value = prop.getString();
                assertEquals("The " + JCR_XMLTEXT + " node " + node.getPath() +
                        " is not exported correctly.",
                        value, exportedVal);
            } else {
                // check the concatenated values sequenceally
                for (int i = 0; i < nodes.size(); i++) {
                    Node node = nodes.get(i);
                    Property prop = node.getProperty(JCR_XMLDATA);
                    value = prop.getString();
                    // the first one
                    if (i == 0) {
                        if (exportedVal.regionMatches(0, value, 0, value.length())) {
                            // test ok, remove the checked part of the text
                            exportedVal = exportedVal.substring(0, value.length());
                        } else {
                            fail("The " + JCR_XMLTEXT + " node " + node.getPath() +
                                    " is not exported correctly: expected: " +
                                    value + " found: " + exportedVal);
                        }
                    }
                    // we assume at the moment that any white space char is possible
                    // between  two adjacent xmltext nodesso we try to match as long
                    // as space characters are at the beginning of the
                    // remaining exported string
                    // todo once this will be specified in the spec more exactly
                    else {
                        // the last one
                        if (exportedVal.regionMatches(0, value, 0, value.length())) {
                            // test ok
                            exportedVal = exportedVal.substring(0, value.length());
                        } else {
                            boolean match = false;
                            int j = 0;
                            char c = exportedVal.charAt(j);
                            while (c == ' ' || c == '\n' || c == '\r'
                                    || c == '\t' || c == '\u000B') {
                                if (exportedVal.regionMatches(j, value, 0, value.length())) {
                                    exportedVal = exportedVal.substring(j, value.length() + j);
                                    match = true;
                                    break;
                                } else {
                                    j++;
                                    c = exportedVal.charAt(j);
                                }
                            }
                            assertTrue("The " + JCR_XMLTEXT + " node " + node.getPath() +
                                    " is not exported correctly: expected: "
                                    + value + " found: " + exportedVal, match);
                        }
                    }
                }
            }
        }
    }

    /**
     * Loops through all child items of a given node to test if items with
     * invalid xml name are exported. (chapter 6.4.2.4 of the JCR
     * specification).
     *
     * @param node the root node of the tree to search
     * @param elem the parent element of the element to which the parent node of
     *             the given node is exported.
     * @throws RepositoryException
     */

    private boolean setExportInvalidXmlNames(Node node, Element elem, boolean isSet)
            throws RepositoryException {

        if (!XMLChar.isValidName(node.getName())) {
            if (elem != null) {
                exportInvalidXmlNames = true;
                isSet = true;
            } else {
                exportInvalidXmlNames = false;
                isSet = true;
            }
        }

        // try properties
        if (!isSet) {
            PropertyIterator iter = node.getProperties();
            while (iter.hasNext()) {
                Property prop = iter.nextProperty();
                if (!exportMultivalProps && prop.getDefinition().isMultiple()) {
                    continue;
                }
                if (!XMLChar.isValidName(prop.getName())) {
                    // property exported?
                    exportInvalidXmlNames = isExportedProp(prop, elem);
                    isSet = true;
                }
            }
        }

        // try child nodes
        if (!isSet && !noRecurse) {
            // search again
            NodeIterator iter = node.getNodes();
            while (iter.hasNext()) {
                Node n = iter.nextNode();
                Element el = findElem(n, elem);
                isSet = setExportInvalidXmlNames(n, el, isSet);
            }
        }
        return isSet;
    }

    /**
     * Set the exportMultivalProps flag. Traverses the tree given by the node
     * and searches a multivalue property which is exported to an attribute of a
     * element of an element tree. (chapter 6.4.2.5 of the JCR specification).
     *
     * @param node
     * @param elem
     * @throws RepositoryException
     */
    private boolean setExportMultivalProps(Node node, Element elem, boolean isSet)
            throws RepositoryException {

        Property[] props = searchMultivalProps(node);
        // multivalued property with valid xml name
        if (props[0] != null) {
            exportMultivalProps = isExportedProp(props[0], elem);
            isSet = true;
        } else {
            // invalid xml named multivalue property exported
            if (props[1] != null) {
                exportMultivalProps = isExportedProp(props[1], elem);
                if (!exportMultivalProps && exportInvalidXmlNames) {
                    isSet = true;
                }
            }
        }

        if (!isSet && !noRecurse) {
            // search again
            NodeIterator iter = node.getNodes();
            while (iter.hasNext()) {
                Node n = iter.nextNode();
                Element el = findElem(n, elem);
                if (el != null) {
                    isSet = setExportMultivalProps(n, el, isSet);
                } else {
                    isSet = false;
                }
            }
        }
        return isSet;
    }

    //-----------------------------------< helper methods >-----------------------------
    /**
     * Search a given node if it contains a multivalue property. As invalid xml
     * names may be exported or not we want to find a multivalue property with
     * valid xml name and also one with an invalid xml name. Returned is a pair
     * of multivalued properties, the first has a valid xml name, the second an
     * invalid one. In case one of these is not found it is replaced by null in
     * the pair.
     *
     * @param node the node to start the search.
     * @return A pair of multivalued properties.
     * @throws RepositoryException
     */
    private Property[] searchMultivalProps(Node node) throws RepositoryException {
        Property[] properties = {null, null};
        for (PropertyIterator props = node.getProperties(); props.hasNext();) {
            Property property = props.nextProperty();
            if (property.getDefinition().isMultiple()) {
                if (XMLChar.isValidName(property.getName())) {
                    properties[0] = property;
                    break;
                } else {
                    properties[1] = property;
                }
            }
        }
        return properties;
    }

    /**
     * Tests if a property is exported or not.
     *
     * @param prop
     * @param elem
     * @return
     * @throws RepositoryException
     */
    private boolean isExportedProp(Property prop, Element elem) throws RepositoryException {
        String name = prop.getName();
        name = XMLChar.isValidName(prop.getName()) ? name : escapeNames(name);
        Attr attr = elem.getAttributeNode(name);
        return (attr != null);
    }

    /**
     * Checks if a given  node is a jcr:xmltext named node and fulfills the
     * condition that the property's value is exported as text.
     *
     * @param node The node to check.
     * @return boolean indicating if the given node fulfills the required
     *         conditions.
     * @throws RepositoryException
     */
    private boolean isXMLTextNode(Node node) throws RepositoryException {
        boolean isTrue = node.getName().equals(JCR_XMLTEXT);
        if (node.hasProperty(JCR_XMLDATA)) {
            Property prop = node.getProperty(JCR_XMLDATA);

            isTrue = !prop.getDefinition().isMultiple()
                    && prop.getType() == PropertyType.STRING
                    // only one property beneath the required jcr:primaryType
                    && getSize(node.getProperties()) == 2
                    && getSize(node.getNodes()) == 0;
        } else {
            isTrue = false;
        }
        return isTrue;
    }

//-----------------------------------< static helper methods >-----------------------------

    /**
     * Decodes a given base 64 encoded string.
     *
     * @param str
     * @return
     * @throws IOException
     */
    private static String decodeBase64(String str) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        Base64.decode(str, bos);
        String decoded = bos.toString("UTF-8");
        return decoded;
    }

    /**
     * Encodes a given stream to base64.
     *
     * @param in the stream to encode.
     * @return the encoded string in base64.
     * @throws IOException if an error occurs.
     */
    private static String encodeBase64(InputStream in) throws IOException {
        StringWriter writer = new StringWriter();
        Base64.encode(in, writer);
        return writer.getBuffer().toString();
    }

    /**
     * Exports values of a multivalue property and concatenate the values
     * separated by a space. (chapter 6.4.4 of the JCR specification).
     *
     * @param prop
     * @param isBinary
     * @return
     * @throws RepositoryException
     */
    private static String exportValues(Property prop, boolean isBinary)
            throws RepositoryException, IOException {
        Value[] vals = prop.getValues();
        // order of multi values is preserved.
        // multival with empty array is exported as empty string
        StringBuffer exportedVal = new StringBuffer();

        String space = "";
        if (isBinary) {
            for (int i = 0; i < vals.length; i++) {
                exportedVal.append(space);
                InputStream in = vals[i].getStream();
                try {
                    exportedVal.append(encodeBase64(in));
                } finally {
                    in.close();
                }
                space = " ";
            }
        } else {
            for (int i = 0; i < vals.length; i++) {
                exportedVal.append(space);
                exportedVal.append(escapeValues(vals[i].getString()));
                space = " ";
            }
        }
        return exportedVal.toString();
    }

    /**
     * Escapes the characters of a given String representing a  Name of an item.
     * The escaping scheme is according the requirements of the JSR 170
     * Specification chapter 6.4.3 . No check performed if the given string is
     * indeed a Name or not.
     *
     * @param name
     * @return
     */
    private static String escapeNames(String name) {
        return EscapeJCRUtil.escapeJCRNames(name);
    }

    /**
     * Escapes the characters of a given string value according the requirements
     * of chapter 6.4.4 of JSR 170 Specification.
     *
     * @param value The string to escape its characters.
     * @return
     */
    private static String escapeValues(String value) {
        return EscapeJCRUtil.escapeJCRValues(value);
    }

//----------------< helpers to retrieve data from an xml document >-------------------

    /**
     * Returns all child elements of the given xml element which have the given
     * name.
     *
     * @param elem
     * @param name
     * @return
     */
    private List<Element> getChildElems(Element elem, String name) {
        List<Element> children = new ArrayList<Element>();
        org.w3c.dom.Node child = elem.getFirstChild();
        while (child != null) {
            if (child.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) {
                if (name.equals("*") || name.equals(child.getNodeName())) {
                    children.add((Element)child);
                }
            }
            child = child.getNextSibling();
        }
        return children;
    }

    /**
     * Counts the number of child elements of the given xml element.
     *
     * @param elem
     * @return
     */
    private long countChildElems(Element elem) {
        long length = 0;
        org.w3c.dom.Node child = elem.getFirstChild();
        while (child != null) {
            if (child.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) {
                length++;
            }
            child = child.getNextSibling();
        }
        return length;
    }

    /**
     * Collects the characters of successive text nodes of the given xml element
     * into an ArrayList.
     *
     * @param elem
     * @return
     */
    private List<String> getChildTextNodeValues(Element elem) {
        List<String> textValues = new ArrayList<String>();
        StringBuffer buf = new StringBuffer();
        org.w3c.dom.Node child = elem.getFirstChild();
        // collect the characters of successive text nodes
        while (child != null) {
            if (child.getNodeType() == org.w3c.dom.Node.TEXT_NODE) {
                while (child != null
                        && child.getNodeType() == org.w3c.dom.Node.TEXT_NODE) {
                    buf.append(child.getNodeValue());
                    child = child.getNextSibling();
                }
                textValues.add(buf.toString());
                buf = new StringBuffer();
            } else {
                child = child.getNextSibling();
            }
        }
        return textValues;
    }

    /**
     * Reads a DOM document from the given XML stream.
     *
     * @param xml XML stream
     * @return DOM document
     * @throws RepositoryException if the document could not be read
     */
    private Document readDocument(InputStream xml) throws RepositoryException {
        try {
            StreamSource source = new StreamSource(xml);
            DOMResult result = new DOMResult();
            TransformerFactory factory = TransformerFactory.newInstance();
            Transformer transformer = factory.newTransformer();
            transformer.transform(source, result);
            return (Document) result.getNode();
        } catch (TransformerException e) {
            throw new RepositoryException("Unable to read xml file", e);
        }
    }

    /**
     * Helper class to separate the attributes with xmlns namespace from the
     * attributes without xmlns namspace. Solely used for the root element of an
     * xml document.
     */
    private class AttributeSeparator {
        private static final String xmlnsURI = "http://www.w3.org/2000/xmlns/";
        private static final String xmlnsPrefix = "xmlns";

        NamedNodeMap attrs;
        Map<String, String> nsAttrs;
        Map<String, String> nonNsAttrs;

        AttributeSeparator(Element elem) {
            nsAttrs = new HashMap<String, String>();
            nonNsAttrs = new HashMap<String, String>();
            attrs = elem.getAttributes();
            separateAttrs();
        }

        public Map<String, String> getNsAttrs() {
            return nsAttrs;
        }

        public Map<String, String> getNonNsAttrs() {
            return nonNsAttrs;
        }

        private void separateAttrs() {
            for (int i = 0; i < attrs.getLength(); i++) {
                Attr attribute = (Attr) attrs.item(i);

                if (xmlnsURI.equals(attribute.getNamespaceURI())) {
                    String localName = attribute.getLocalName();
                    // ignore setting default namespace
                    if (xmlnsPrefix.equals(localName)) {
                        continue;
                    }
                    nsAttrs.put(localName, attribute.getValue());
                } else {
                    nonNsAttrs.put(attribute.getName(), attribute.getValue());
                }
            }
        }
    }
}
