/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2017 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://oss.oracle.com/licenses/CDDL+GPL-1.1
 * or LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.xml.messaging.saaj.soap.impl;

import java.util.Iterator;
import java.util.Locale;
import java.util.logging.Level;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import com.sun.xml.messaging.saaj.util.SAAJUtil;

import com.sun.xml.messaging.saaj.SOAPExceptionImpl;
import com.sun.xml.messaging.saaj.soap.SOAPDocument;
import com.sun.xml.messaging.saaj.soap.SOAPDocumentImpl;
import com.sun.xml.messaging.saaj.soap.StaxBridge;
import com.sun.xml.messaging.saaj.soap.name.NameImpl;
import javax.xml.soap.Name;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPBodyElement;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPFault;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * The implementation of SOAP-ENV:BODY or the SOAPBody abstraction.
 *
 * @author Anil Vijendran (anil@sun.com)
 */
public abstract class BodyImpl extends ElementImpl implements SOAPBody {
    private SOAPFault fault;
//  private XMLStreamReaderToXMLStreamWriter staxBridge;
    private StaxBridge staxBridge;
    private boolean payloadStreamRead = false;
    
    protected BodyImpl(SOAPDocumentImpl ownerDoc, NameImpl bodyName) {
        super(ownerDoc, bodyName);
    }

    public BodyImpl(SOAPDocumentImpl ownerDoc, Element domElement) {
        super(ownerDoc, domElement);
    }

    protected abstract NameImpl getFaultName(String name);
    protected abstract boolean isFault(SOAPElement child);
    protected abstract SOAPBodyElement createBodyElement(Name name);
    protected abstract SOAPBodyElement createBodyElement(QName name);
    protected abstract SOAPFault createFaultElement();
    protected abstract QName getDefaultFaultCode();

    @Override
    public SOAPFault addFault() throws SOAPException {
        if (hasFault()) {
            log.severe("SAAJ0110.impl.fault.already.exists");
            throw new SOAPExceptionImpl("Error: Fault already exists");
        }

        fault = createFaultElement();

        addNode(fault);

        fault.setFaultCode(getDefaultFaultCode());
        fault.setFaultString("Fault string, and possibly fault code, not set");

        return fault;
    }

    @Override
    public SOAPFault addFault(
        Name faultCode,
        String faultString,
        Locale locale)
        throws SOAPException {
        
        SOAPFault fault = addFault();
        fault.setFaultCode(faultCode);
        fault.setFaultString(faultString, locale);
        return fault;
    }

    @Override
   public SOAPFault addFault(
        QName faultCode,
        String faultString,
        Locale locale)
        throws SOAPException {

        SOAPFault fault = addFault();
        fault.setFaultCode(faultCode);
        fault.setFaultString(faultString, locale);
        return fault;
    }

    @Override
    public SOAPFault addFault(Name faultCode, String faultString)
        throws SOAPException {

        SOAPFault fault = addFault();
        fault.setFaultCode(faultCode);
        fault.setFaultString(faultString);
        return fault;
    }

    @Override
    public SOAPFault addFault(QName faultCode, String faultString)
        throws SOAPException {

        SOAPFault fault = addFault();
        fault.setFaultCode(faultCode);
        fault.setFaultString(faultString);
        return fault;
    }

    void initializeFault() {
        FaultImpl flt = (FaultImpl) findFault();
        fault = flt;
    }

    protected SOAPElement findFault() {
        Iterator<Node> eachChild = getChildElementNodes();
        while (eachChild.hasNext()) {
            SOAPElement child = (SOAPElement) eachChild.next();
            if (isFault(child)) {
                return child;
            }
        }

        return null;
    }

    @Override
    public boolean hasFault() {
        QName payloadQName = getPayloadQName();
        return getFaultQName().equals(payloadQName);
    }

    private Object getFaultQName() {
        return new QName(getNamespaceURI(), "Fault");
    }

    @Override
    public SOAPFault getFault() {
        if (hasFault()) {
            if (fault == null) {
                //initialize fault member
                fault = (SOAPFault) getSoapDocument().find(getFirstChildElement());
            }
            return fault;
        }
        return null;
    }

    @Override
    public SOAPBodyElement addBodyElement(Name name) throws SOAPException {
        SOAPBodyElement newBodyElement =
            (SOAPBodyElement) ElementFactory.createNamedElement(
                ((SOAPDocument) getOwnerDocument()).getDocument(),
                name.getLocalName(),
                name.getPrefix(),
                name.getURI());
        if (newBodyElement == null) {
            newBodyElement = createBodyElement(name);
        }
        addNode(newBodyElement);
        return newBodyElement;
    }

    @Override
    public SOAPBodyElement addBodyElement(QName qname) throws SOAPException {
        SOAPBodyElement newBodyElement =
            (SOAPBodyElement) ElementFactory.createNamedElement(
                ((SOAPDocument) getOwnerDocument()).getDocument(),
                qname.getLocalPart(),
                qname.getPrefix(),
                qname.getNamespaceURI());
        if (newBodyElement == null) {
            newBodyElement = createBodyElement(qname);
        }
        addNode(newBodyElement);
        return newBodyElement;
    }

    @Override
    public void setParentElement(SOAPElement element) throws SOAPException {

        if (!(element instanceof SOAPEnvelope)) {
            log.severe("SAAJ0111.impl.body.parent.must.be.envelope");
            throw new SOAPException("Parent of SOAPBody has to be a SOAPEnvelope");
        }
        super.setParentElement(element);
    }

    @Override
    protected SOAPElement addElement(Name name) throws SOAPException {
        return addBodyElement(name);
    }

    @Override
    protected SOAPElement addElement(QName name) throws SOAPException {
        return addBodyElement(name);
    }

    //    public Node insertBefore(Node newElement, Node ref) throws DOMException {
    //        if (!(newElement instanceof SOAPBodyElement) && (newElement instanceof SOAPElement)) {
    //            newElement = new ElementWrapper((ElementImpl) newElement);
    //        }
    //        return super.insertBefore(newElement, ref);
    //    }
    //
    //    public Node replaceChild(Node newElement, Node ref) throws DOMException {
    //        if (!(newElement instanceof SOAPBodyElement) && (newElement instanceof SOAPElement)) {
    //            newElement = new ElementWrapper((ElementImpl) newElement);
    //        }
    //        return super.replaceChild(newElement, ref);
    //    }

    @Override
    public SOAPBodyElement addDocument(Document document)
        throws SOAPException {
        /*
                
                Element rootNode =
                    document.getDocumentElement();
                // Causes all deferred nodes to be inflated
                rootNode.normalize();
                adoptElement(rootNode);
                SOAPBodyElement bodyElement = (SOAPBodyElement) convertToSoapElement(rootNode);
                addNode(bodyElement);
                return bodyElement;
        */
        ///*
        SOAPBodyElement newBodyElement = null;
        DocumentFragment docFrag = document.createDocumentFragment();
        Element rootElement = document.getDocumentElement();
        if(rootElement != null) {
            docFrag.appendChild(rootElement);

            Document ownerDoc = getOwnerDocument();
            // This copies the whole tree which could be very big so it's slow.
            // However, it does have the advantage of actually working.
            org.w3c.dom.Node replacingNode = ownerDoc.importNode(docFrag, true);
            // Adding replacingNode at the last of the children list of body
            addNode(replacingNode);
            Iterator<javax.xml.soap.Node> i =
                getChildElements(NameImpl.copyElementName(rootElement));
            // Return the child element with the required name which is at the
            // end of the list
            while(i.hasNext())
                newBodyElement = (SOAPBodyElement) i.next();
        }
        return newBodyElement;
        //*/        
    }

    @Override
    protected SOAPElement convertToSoapElement(Element element) {
        final Node soapNode = getSoapDocument().findIfPresent(element);
        if ((soapNode instanceof SOAPBodyElement) &&
            //this check is required because ElementImpl currently 
            // implements SOAPBodyElement
            !(soapNode.getClass().equals(ElementImpl.class))) {
            return (SOAPElement) soapNode;
        } else {
            return replaceElementWithSOAPElement(
                element,
                (ElementImpl) createBodyElement(NameImpl
                    .copyElementName(element)));
        }
    }

    @Override
    public SOAPElement setElementQName(QName newName) throws SOAPException {
        log.log(Level.SEVERE,
                "SAAJ0146.impl.invalid.name.change.requested",
                new Object[] {elementQName.getLocalPart(),
                              newName.getLocalPart()});
        throw new SOAPException("Cannot change name for "
                                + elementQName.getLocalPart() + " to "
                                + newName.getLocalPart());
    }

    @Override
    public Document extractContentAsDocument() throws SOAPException {

        Iterator<javax.xml.soap.Node> eachChild = getChildElements();
        javax.xml.soap.Node firstBodyElement = null;

        while (eachChild.hasNext() &&
               !(firstBodyElement instanceof SOAPElement))
            firstBodyElement = (javax.xml.soap.Node) eachChild.next();

        boolean exactlyOneChildElement = true;
        if (firstBodyElement == null)
            exactlyOneChildElement = false;
        else {
            for (org.w3c.dom.Node node = firstBodyElement.getNextSibling();
                 node != null;
                 node = node.getNextSibling()) {

                if (node instanceof Element) {
                    exactlyOneChildElement = false;
                    break;
                }
            }
        }

        if(!exactlyOneChildElement) {
            log.log(Level.SEVERE,
                    "SAAJ0250.impl.body.should.have.exactly.one.child");
            throw new SOAPException("Cannot extract Document from body");
        }

        Document document = null;
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance("com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl", SAAJUtil.getSystemClassLoader());
            factory.setNamespaceAware(true);
            DocumentBuilder builder = factory.newDocumentBuilder();
            document = builder.newDocument();

            Element rootElement = (Element) document.importNode(
                                                firstBodyElement,
                                                true);

            document.appendChild(rootElement);

        } catch(Exception e) {
            log.log(Level.SEVERE,
                    "SAAJ0251.impl.cannot.extract.document.from.body");
            throw new SOAPExceptionImpl(
                "Unable to extract Document from body", e);
        }
            
        firstBodyElement.detachNode();

        return document;
    }

    private void materializePayloadWrapException() {
        try {
            materializePayload();
        } catch (SOAPException e) {
            throw new RuntimeException(e);
        }
    }
    private void materializePayload() throws SOAPException {
        if (staxBridge != null) {
            if (payloadStreamRead) {
                //the payload has already been read via stream reader and the 
                //stream has been exhausted already. Throw an
                //exception since we are now trying to materialize as DOM and
                //there is no stream left to read
                throw new SOAPException("SOAPBody payload stream has been fully read - cannot materialize as DOM!");
            }
            try {
                staxBridge.bridgePayload();
                staxBridge = null;
                payloadStreamRead = true;
            } catch (XMLStreamException e) {
                throw new SOAPException(e);
            }
        }
    }
    
    @Override
    public boolean hasChildNodes() {
        boolean hasChildren = super.hasChildNodes();
        //to answer this question we need to know _whether_ we have at least one child
        //So no need to materialize body if we already know we have a header child
        if (!hasChildren) {
            materializePayloadWrapException();
        }
        return super.hasChildNodes();
    }

    @Override
    public NodeList getChildNodes() {
        materializePayloadWrapException();
        return super.getChildNodes();
    }

    @Override
    public Node getFirstChild() {
        Node child = super.getFirstChild();
        if (child == null) {
            materializePayloadWrapException();
        }
        return super.getFirstChild();
    }
    
    public Node getFirstChildNoMaterialize() {
        return super.getFirstChild();
    }
   
    @Override
    public Node getLastChild() {
        materializePayloadWrapException();
        return super.getLastChild();
    }
    
    XMLStreamReader getPayloadReader() {
        return staxBridge.getPayloadReader();
    }

    void setStaxBridge(StaxBridge bridge) {
        this.staxBridge = bridge;
    }

    StaxBridge getStaxBridge() {
        return staxBridge;
    }

    void setPayloadStreamRead() {
        this.payloadStreamRead = true;
    }

    QName getPayloadQName() {
        if (staxBridge != null) {
        	return staxBridge.getPayloadQName();
        } else {
            //not lazy - Just get first child element and return its name
            Element elem = getFirstChildElement();
            if (elem != null) {
                String ns = elem.getNamespaceURI();
                String pref = elem.getPrefix();
                String local = elem.getLocalName();
                if (pref != null) return new QName(ns, local, pref);
                if (ns != null) return new QName(ns, local);
                return new QName(local);
            }
        }
        return null;
    }
    
    String getPayloadAttributeValue(String attName) {
        if (staxBridge != null) {
            return staxBridge.getPayloadAttributeValue(attName);
        } else {
            //not lazy -Just get first child element and return its attribute
            Element elem = getFirstChildElement();
            if (elem != null) {
                return elem.getAttribute(getLocalName());
            }
        }
        return null;
    }

    String getPayloadAttributeValue(QName attNAme) {
        if (staxBridge != null) {
            return staxBridge.getPayloadAttributeValue(attNAme);
        } else {
            //not lazy -Just get first child element and return its attribute
            Element elem = getFirstChildElement();
            if (elem != null) {
                return elem.getAttributeNS(attNAme.getNamespaceURI(), attNAme.getLocalPart());
            }
        }
        return null;
    }

    public boolean isLazy() {
        return (staxBridge != null && !payloadStreamRead);
    }

}
