/*
 * 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.NotExecutableException;
import org.xml.sax.SAXException;

import javax.jcr.RepositoryException;
import javax.jcr.Property;
import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.PropertyType;
import javax.jcr.PropertyIterator;
import javax.jcr.NamespaceException;
import javax.jcr.ImportUUIDBehavior;
import javax.jcr.ItemExistsException;
import javax.jcr.nodetype.ConstraintViolationException;
import java.io.IOException;

/**
 * <code>DocumentViewImportTest</code> Tests importXML and
 * getImportContentHandler methods of the Workspace and Session class. Also
 * tests the UuidBehaviour flag.
 *
 */
public class DocumentViewImportTest extends AbstractImportXmlTest {

    private String JCR_XMLTEXT;
    private String JCR_XMLCHAR;

    private boolean withHandler;
    private boolean withWorkspace;

    public void setUp() throws Exception {
        super.setUp();
        JCR_XMLTEXT = superuser.getNamespacePrefix(NS_JCR_URI) + ":xmltext";
        JCR_XMLCHAR = superuser.getNamespacePrefix(NS_JCR_URI) + ":xmlcharacters";
    }

    public void tearDown() throws Exception {
        file.delete();
        super.tearDown();
    }

    public void testWorkspaceImportXml() throws Exception {
        withHandler = false;
        withWorkspace = WORKSPACE;
        doTestImportXML();
    }

    public void testSessionImportXml() throws Exception {
        withHandler = false;
        withWorkspace = SESSION;
        doTestImportXML();
    }

    public void testWorkspaceGetImportContentHandler() throws Exception {
        withHandler = true;
        withWorkspace = SESSION;
        doTestGetImportContentHandler();
    }

    public void testSessionGetImportContentHandler() throws Exception {
        withHandler = true;
        withWorkspace = WORKSPACE;
        doTestGetImportContentHandler();
    }

    /**
     * Tests importXML method with uuidBehaviour IMPORT_UUID_CREATE_NEW. It
     * imports the document created with createSimpleDocument method and checks
     * the imported tree according the rules outlined in chapter 7.3.2 of the
     * specification.
     * <p>
     * Additionally it checks the uuidBehaviour flag if the jcr:uuid property is
     * respected during import.
     *
     * @throws RepositoryException
     * @throws IOException
     * @throws SAXException
     * @throws NotExecutableException
     */
    public void doTestImportXML() throws Exception {

        importXML(target, createSimpleDocument(), uuidBehaviour, withWorkspace);

        // some implementations may require a refresh to get content
        // added diretly to the workspace
        if (withWorkspace) {
            session.refresh(false);
        }

        performTests();
    }

    /**
     * Tests getImportContentHandler method with uuidBehaviour
     * IMPORT_UUID_CREATE_NEW. It imports the document created with
     * createSimpleDocument method and checks the imported tree according the
     * rules outlined in chapter 7.3.2 of the specification.
     * <p>
     * Additionally it checks the uuidBehaviour flag if the jcr:uuid property is
     * respected during import.
     *
     * @throws RepositoryException
     * @throws SAXException
     * @throws IOException
     * @throws NotExecutableException
     */
    public void doTestGetImportContentHandler() throws Exception {

        importWithHandler(target, createSimpleDocument(), uuidBehaviour, withWorkspace);

        // some implementations may require a refresh to get content
        // added diretly to the workspace
        if (withWorkspace) {
            session.refresh(false);
        }

        performTests();
    }


    private void performTests() throws Exception {

        checkImportSimpleXMLTree();
        checkNamespaceAdded();
        if (!respectMixRef) {
            throw new NotExecutableException("ImportXML tests with " +
                    "uuidBehaviour flag not executable.");
        } else {
            checkImportDocumentView_IMPORT_UUID_CREATE_NEW();
            checkImportDocumentView_IMPORT_UUID_COLLISION_REMOVE_EXISTING();
            checkImportDocumentView_IMPORT_UUID_COLLISION_REPLACE_EXISTING();
            checkImportDocumentView_IMPORT_UUID_COLLISION_THROW();
        }
    }

    /**
     * Tests if the simple xml document defined in createSimpleDocument() is
     * imported correctly according the specification rules given in  7.3.2
     */
    public void checkImportSimpleXMLTree() throws RepositoryException, IOException {
        Node parent = (Node) session.getItem(target);

        try {
            // check the node names
            String prefix = session.getNamespacePrefix(unusedURI);
            String rootName = prefix + ":" + rootElem;
            //String rootName = rootElem;
            Node rootNode = parent.getNode(rootName);
            Node child = rootNode.getNode(childElem);
            Node xmlTextNode = rootNode.getNode(xmltextElem);
            Node grandChild = xmlTextNode.getNode(grandChildElem);

            // check xmltext
            checkXmlTextNode(xmlTextNode);

            // check the property names and values
            Property prop = grandChild.getProperty(attributeName);
            Property prop2 = xmlTextNode.getProperty(attributeName);
            String value = prop.getString();
            String value2 = prop2.getString();
            assertEquals("Value " + attributeValue + " of attribute " +
                    attributeName + " is imported to different property values.", value, value2);
            assertEquals("Value " + attributeValue + "  of attribute " +
                    attributeName + " is not correctly imported.", value, attributeValue);

            // check the encoded names and values
            Property decodedProp;
            // is decoded
            try {
                child.getNode(decodedElemName);
                decodedProp = child.getProperty(decodedAttributeName);
                String propVal = decodedProp.getString();
                // both possibilities
                if (!propVal.equals(encodedAttributeValue)
                        && !propVal.equals(decodedAttributeValue)) {
                    fail("Value " + encodedAttributeValue + "  of attribute " +
                            decodedAttributeName + " is not correctly imported.");
                }

            } catch (PathNotFoundException pnfe) {
                try {
                    // is not decoded
                    child.getNode(encodedElemName);
                    decodedProp = child.getProperty(encodedAttributeName);
                    String propVal = decodedProp.getString();
                    // both possibilities
                    if (!propVal.equals(encodedAttributeValue)
                            && !propVal.equals(decodedAttributeValue)) {
                        fail("Value " + encodedAttributeValue + "  of attribute " +
                                encodedAttributeName + " is not correctly imported.");
                    }
                } catch (PathNotFoundException pnfe2) {
                    fail("XML Element " + encodedElemName + " or attribute "
                            + encodedAttributeName + " not imported: " + pnfe2);
                }
            }
        } catch (PathNotFoundException pne) {
            fail("Element or attribute is not imported: " + pne);
        }
    }

    /**
     * Tests if xmltext in a body of a xml element is correctly imported to a
     * node with name jcr:xmltext and that the value of the text is stored in
     * the singlevalued jcr:xmlcharacters property of String type.
     *
     * @throws RepositoryException
     */
    public void checkXmlTextNode(Node node) throws RepositoryException, IOException {

        if (node.hasNode(JCR_XMLTEXT)) {
            Node xmlNode = node.getNode(JCR_XMLTEXT);
            if (xmlNode.hasProperty(JCR_XMLCHAR)) {
                Property prop = xmlNode.getProperty(JCR_XMLCHAR);
                // correct type?
                assertTrue("Property " + prop.getPath() + " is not of type String.",
                        prop.getType() == PropertyType.STRING);
                // correct text?
                // todo remove the trim as only the white spaces of the current text should be collected
                assertEquals("Xml text is not correctly stored.",
                        xmltext.trim(), prop.getString().trim());
                // only jcr:xmlcharacters property beneath the jcr:primaryType
                PropertyIterator iter = xmlNode.getProperties();
                assertTrue(JCR_XMLCHAR + " is not the only property beneath " +
                        jcrPrimaryType + " in a " + JCR_XMLTEXT + " node.", getSize(iter) == 2);
            } else {
                fail("Xmltext not stored in property named " + JCR_XMLCHAR);
            }
        } else {
            fail("Xmltext not imported to Node named " + JCR_XMLTEXT);
        }
    }

    /**
     * Checks if a namespace not yet existing in the repository is registered
     * after an according document import.
     *
     * @throws RepositoryException
     * @throws IOException
     */
    public void checkNamespaceAdded() throws RepositoryException, IOException {
        try {
            assertEquals("URI not correctly imported.", nsp.getURI(unusedPrefix), unusedURI);
            assertEquals("Prefix not correctly imported", nsp.getPrefix(unusedURI), unusedPrefix);
        } catch (NamespaceException nse) {
            fail("Namespace " + unusedPrefix + ":" + unusedURI +
                    " not imported during document view import.");
        }
    }

    //------------------< uuid collision behaviour tests >--------------------------
    // we create a node named referenced below the testRootNode, also we create a xml
    // document with an element named rootElem having the uuid of the referenced node
    // as attribute and we import this document below the refTargetNode.

    /**
     * Checks {@link ImportUUIDBehavior#IMPORT_UUID_CREATE_NEW} i.e. that a node
     * receives a new uuid when imported in any case.
     */
    public void checkImportDocumentView_IMPORT_UUID_CREATE_NEW() throws Exception {

        String uuid = createReferenceableNode(referenced);
        // import a document with a element having the same uuid as the node referenced
        importRefNodeDocument(refTarget, uuid, ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW, withWorkspace, withHandler);

        // different uuid?
        Node node = refTargetNode.getNode(rootElem);
        String rootElemUUID = node.getUUID();
        assertFalse("Imported node " + rootElem + " has a UUID which is " +
                "yet assigned to another node", uuid.equals(rootElemUUID));
    }

    /**
     * Checks ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING i.e that
     * the existing node is removed in case of uuid collision.
     */
    public void checkImportDocumentView_IMPORT_UUID_COLLISION_REMOVE_EXISTING()
            throws Exception {

        String uuid = createReferenceableNode(referenced);
        // import a document with a element having the same uuid as the node referenced
        importRefNodeDocument(refTarget, uuid, ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING,
                withWorkspace, withHandler);

        try {
            // should be removed now
            testRootNode.getNode(referenced);
            fail("UUID behavior IMPORT_UUID_COLLISION_REMOVE_EXISTING test is failed: " +
                    "existing node not removed");
        } catch (PathNotFoundException pnfe) {
            // ok
        }
        try {
            // should be there
            refTargetNode.getNode(rootElem);
        } catch (PathNotFoundException pnfe) {
            fail("UUID behavior IMPORT_UUID_COLLISION_REMOVE_EXISTING test is failed: " +
                    "imported node not in its correct place.");
        }
    }

    /**
     * Checks ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING i.e that
     * the existing node is replaced by the imported one node when uuid
     * collision occurs.
     */
    public void checkImportDocumentView_IMPORT_UUID_COLLISION_REPLACE_EXISTING()
            throws Exception {

        String uuid = createReferenceableNode(referenced);
        // import a document with a element having the same uuid as the node referenced
        importRefNodeDocument(refTarget, uuid, ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING,
                withWorkspace, withHandler);

        // should be replaced i.e should be modified, in case Workspace method is used
        //  we cannot decide unless we have some additional property of the imported node.
        if (!withWorkspace) {
            Node node = testRootNode.getNode(rootElem);
            assertTrue("Node " + node.getPath() + " not replaced during " +
                    "import with IMPORT_UUID_COLLISION_REPLACE_EXISTING", node.hasProperty(propertyName1));
        }
    }

    /**
     * Checks ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW i.e that a
     * ItemExistsException is thrown in case of importing with an input stream
     * or a SAXException is thrown in case of importing with a ContentHandler.
     *
     * @throws RepositoryException
     * @throws IOException
     */
    public void checkImportDocumentView_IMPORT_UUID_COLLISION_THROW()
            throws Exception {

        String uuid = createReferenceableNode(referenced);
        try {
            importRefNodeDocument(refTarget, uuid, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW,
                    withWorkspace, withHandler);
            fail("UUID behavior IMPORT_UUID_COLLISION_THROW test is failed: " +
                    "should throw an Exception.");
        } catch (ItemExistsException e) {
            if (!withHandler) {
                // ok
            } else {
                throw e;
            }
        } catch (SAXException e) {
            if (withHandler) {
                // ok
            } else {
                throw e;
            }
        }
    }
  //-------------------------------< exception tests >--------------------------------------
    /**
     * Tests correct failure of importing a element wit the same UUID as the target node or
     * an ancestor of it in case of uuidBehavior IMPORT_UUID_COLLISION_REMOVE_EXISTING.
     *
     * The imported document contains a element with jcr:uuid attribute the same as the
     * parent of the import target.
     */
    public void doTestSameUUIDAtAncestor(boolean withWorkspace, boolean withHandler)
            throws Exception {

        String uuid = createReferenceableNode(referenced);
        Node node = testRootNode.getNode(referenced);
        Node node2 = node.addNode("newParent");
        session.save();
        // import a document with a element having the same uuid as the node referenced
        try {
            importRefNodeDocument(node2.getPath(), uuid,
                ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING,
                    withWorkspace, withHandler);
            fail("UUID collision with an ancestor of the target node hould throw a " +
                    "SAXException or a ConstraintViolationException in case of " +
                    "uuidBehavior IMPORT_UUID_COLLISION_REMOVE_EXISTING.");
        } catch(SAXException se) {
            if (!withHandler) {
                throw se;
            }
            // ok
        } catch (ConstraintViolationException cve) {
            if (withHandler) {
                throw cve;
            }
            // ok
        }
    }

    public void testSameUUIDAtAncestorWorkspaceHandler() throws Exception {
        doTestSameUUIDAtAncestor(WORKSPACE, CONTENTHANDLER);
    }

    public void testSameUUIDAtAncestorWorkspace() throws Exception {
        doTestSameUUIDAtAncestor(WORKSPACE, STREAM);
    }

    public void testSameUUIDAtAncestorSessionHandler() throws Exception  {
        doTestSameUUIDAtAncestor(SESSION, CONTENTHANDLER);
    }

    public void testSameUUIDAtAncestorSession() throws Exception {
        doTestSameUUIDAtAncestor(SESSION, STREAM);
    }
}
