/**
 * Copyright (c) 2007-2012 EBM WebSourcing, 2012-2024 Linagora
 * 
 * This program/library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 2.1 of the License, or (at your
 * option) any later version.
 * 
 * This program/library 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 Lesser General Public License
 * for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program/library; If not, see http://www.gnu.org/licenses/
 * for the GNU Lesser General Public License version 2.1.
 */
package org.ow2.petals.binding.soap.util;

import static javax.jbi.messaging.NormalizedMessageProperties.PROTOCOL_HEADERS;
import static org.ow2.petals.binding.soap.SoapConstants.SOAP.FAULT_CLIENT;
import static org.ow2.petals.binding.soap.SoapConstants.WSSE.WSSE_QNAME;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

import javax.activation.DataHandler;
import javax.jbi.messaging.Fault;
import javax.jbi.messaging.MessagingException;
import javax.jbi.messaging.NormalizedMessage;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.transform.Source;

import org.apache.axiom.attachments.Attachments;
import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMAttribute;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMFactory;
import org.apache.axiom.om.OMNamespace;
import org.apache.axiom.om.OMNode;
import org.apache.axiom.om.OMText;
import org.apache.axiom.om.OMXMLBuilderFactory;
import org.apache.axiom.soap.SOAPBody;
import org.apache.axiom.soap.SOAPEnvelope;
import org.apache.axiom.soap.SOAPFactory;
import org.apache.axiom.soap.SOAPFault;
import org.apache.axiom.soap.SOAPHeader;
import org.apache.axiom.util.stax.xop.ContentIDGenerator;
import org.apache.axiom.util.stax.xop.OptimizationPolicy;
import org.apache.axiom.util.stax.xop.XOPEncodingStreamWriter;
import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
import org.apache.axis2.client.ServiceClient;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.util.XMLUtils;
import org.ow2.petals.binding.soap.SoapConstants;
import org.ow2.petals.commons.log.Level;
import org.ow2.petals.component.framework.api.message.Exchange;
import org.ow2.petals.component.framework.util.SourceUtil;
import org.ow2.petals.jbi.xml.BytesSource;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import com.ebmwebsourcing.easycommons.stream.EasyByteArrayOutputStream;
import com.ebmwebsourcing.easycommons.xml.DocumentBuilders;
import com.ebmwebsourcing.easycommons.xml.JVMDocumentBuilders;
import com.ebmwebsourcing.easycommons.xml.XMLOutputFactories;

/**
 * A marshaller to create JBI message from SOAP ones and vice versa.
 * 
 * TODO there is many duplicate code in here
 * 
 * {@link #createSOAPBodyContent(NormalizedMessage, ServiceClient)} is used when receiving new JBI Message.
 * 
 * {@link #createSOAPEnvelope(SOAPFactory, NormalizedMessage, Logger)} and
 * {@link #fillSOAPBodyWithAttachments(NormalizedMessage, SOAPFactory, MessageContext)} are used when receiving response
 * to messages sent TO JBI (with sendSync).
 * 
 * The rest is used to create JBI messages either when receiving a SOAP message or sending the response to a JBI message
 * sent TO SOAP.
 * 
 * @author Christophe HAMERLING - EBM WebSourcing
 */
public final class Marshaller {

    /**
     * No constructor because <code>Marshaller</code> is an utility class.
     */
    private Marshaller() {
        // NOP
    }

    /**
     * Create the JBI message from the OMElement object.
     * 
     * @param from
     * @param to
     * @throws MessagingException
     */
    public static void copyAttachments(final OMElement from, final NormalizedMessage to)
            throws MessagingException {

        // get attachments
        final Iterator<OMNode> iter = from.getChildren();
        while (iter.hasNext()) {
            final OMNode node = iter.next();
            if (node instanceof OMElement) {
                final OMElement element = (OMElement) node;
                // (all the nodes that have an href attributes are
                // attachments)
                final OMAttribute attr = element.getAttribute(new QName("href"));
                if ((attr != null) && (node instanceof OMText)) {
                    if ("Include".equalsIgnoreCase(element.getLocalName())
                            && "http://www.w3.org/2004/08/xop/include".equalsIgnoreCase(element
                                    .getNamespace().getNamespaceURI())) {
                        final String attachmentId = attr.getAttributeValue().substring(4);
                        final OMText binaryNode = (OMText) node;
                        final DataHandler dh = (DataHandler) binaryNode.getDataHandler();
                        to.addAttachment(attachmentId, dh);
                    }
                }
            }
        }
    }

    /**
     * Create the SOAPBody content. The JBI content is used as root element. All the JBI attachments are added as root
     * element children.
     * 
     * @param nm
     * @param client
     * @return the soap body content or null if {@link NormalizedMessage} is empty.
     * @throws XMLStreamException
     * @throws UnsupportedEncodingException
     */
    public static OMElement createSOAPBodyContent(final NormalizedMessage nm, final ServiceClient client)
                    throws XMLStreamException, UnsupportedEncodingException {

        OMElement document = null;
        final Source src = nm.getContent();

        if (src != null) {
            // create the root element
            final OMFactory fac = OMAbstractFactory.getOMFactory();
            document = OMXMLBuilderFactory.createOMBuilder(src).getDocumentElement();
            // it will be sent, so everything will be read: we can build it and release the builder now
            document.close(true);
            // add attachments if any
            // TODO this code is very similar to fillSOAPBodyWithAttachements...
            if (!nm.getAttachmentNames().isEmpty()) {

                // enable MTOM
                client.getOptions().setProperty(Constants.Configuration.ENABLE_MTOM, Constants.VALUE_TRUE);

                // Add JBI attachments to the document element
                @SuppressWarnings("unchecked")
                final Set<String> names = nm.getAttachmentNames();
                for (final String key : names) {
                    final DataHandler attachment = nm.getAttachment(key);
                    final OMElement attachRefElt = AttachmentHelper.hasAttachmentElement(document, attachment, key);
                    if (attachRefElt != null) {
                        // An element references the attachment, we replace it
                        // by
                        // itself using AXIOM API (It's a requirement of Axis2)
                        attachRefElt
                                .getFirstChildWithName(new QName("http://www.w3.org/2004/08/xop/include", "Include"))
                                .detach();
                        final OMText attach = fac.createOMText(attachment, true);
                        attachRefElt.addChild(attach);
                    }
                }
            }
        }

        return document;
    }

    /**
     * Create a SOAPBody from a source
     * 
     * @param factory
     *            a SOAP factory
     * @param envelope
     *            a SOAP envelope
     * @param source
     *            a source
     * @return the SOAP body
     */
    static SOAPBody createSOAPBody(final SOAPFactory factory, final SOAPEnvelope envelope, final Source source,
            final boolean isFault, final Logger logger) {

        final OMElement jbiNmContent = OMXMLBuilderFactory.createOMBuilder(factory, source).getDocumentElement();
        // it will be sent, so everything will be read: we can build it and release the builder now
        jbiNmContent.close(true);

        final SOAPBody body = factory.createSOAPBody(envelope);

        if (isFault) {
            final SOAPFault soapFault = SOAPFaultHelper.createBusinessSOAPFault(factory, jbiNmContent);
            body.addFault(soapFault);
        } else {
            body.addChild(jbiNmContent);
        }

        return body;
    }

    /**
     * Creates a SOAP response from a JBI 'OUT' message of a JBI fault.
     * 
     * @param factory
     *            soap factory
     * @param nm
     *            NormalizedMessage containing the JBI response
     * @param logger
     * @return a SOAPEnveloppe created from the nm NomalizedMessage content
     */
    @SuppressWarnings("unchecked")
    public static SOAPEnvelope createSOAPEnvelope(final SOAPFactory factory, final NormalizedMessage nm,
            final Logger logger) {

        /*
         * Create and fill the Soap body with the content of the Normalized
         * message
         */
        final Source source;
        Map<String, DocumentFragment> protocolHeadersProperty = null;
        if ((nm == null) || (nm.getContent() == null)) {
            final Document doc = DocumentBuilders.newDocument();
            
            final Element responseElement = doc.createElement("Response");
            responseElement.setNodeValue("Done");
            doc.appendChild(responseElement);
            source = SourceUtil.createSource(doc);
        } else {
            source = nm.getContent();
            final Object protocolHeadersPropertyObject = nm.getProperty(PROTOCOL_HEADERS);
            if ((protocolHeadersPropertyObject != null)
                    && (protocolHeadersPropertyObject instanceof Map<?, ?>)) {
                protocolHeadersProperty = (Map<String, DocumentFragment>) protocolHeadersPropertyObject;
            }
        }

        final SOAPEnvelope responseEnv = factory.createSOAPEnvelope();

        Marshaller.createSOAPHeader(factory, responseEnv, protocolHeadersProperty, logger);
        Marshaller.createSOAPBody(factory, responseEnv, source, nm instanceof Fault, logger);

        return responseEnv;
    }

    /**
     * Create a SOAPHeader from a source
     * 
     * @param factory
     * @param envelope
     * @param protocolHeadersProperty
     */
    private static void createSOAPHeader(final SOAPFactory factory, final SOAPEnvelope envelope,
            final Map<String, DocumentFragment> protocolHeadersProperty, final Logger logger) {

        if (protocolHeadersProperty != null) {
            final SOAPHeader header = factory.createSOAPHeader(envelope);

            for (final DocumentFragment docfrag : protocolHeadersProperty.values()) {
                final Node node = docfrag.getFirstChild();
                if (node instanceof Element) {
                    try {
                        header.addChild(XMLUtils.toOM((Element) node));
                    } catch (final Exception e) {
                        logger.log(Level.WARNING,
                                "Error parsing the response from JBI service to a SOAPHeader, skipping it", e);
                    }
                }
            }
        }
    }

    /**
     * Create a the JBI payload from the SOAP body. The SOAP envelope is
     * required to get namespaces.
     * 
     * @param msgContext
     *            the message context containing the SOAP envelope
     * @param jbiMsg
     *            the JBI message that will be filled
     * @param axis1Compatibility
     *            a flag for Axis compatibility
     * @param logger
     * @throws MessagingException
     *             if there is an error when creating the source content
     */
    public static void fillJBIMessage(final MessageContext msgContext, final NormalizedMessage jbiMsg,
            final boolean axis1Compatibility, final Logger logger) throws MessagingException {

        final SOAPEnvelope envelope;
        if (axis1Compatibility) {
            // mutiref to document
            envelope = AxiomSOAPEnvelopeFlattener.flatten(msgContext.getEnvelope());
        } else {
            envelope = msgContext.getEnvelope();
        }
        final Source source = createSourceContent(envelope);

        final Attachments attachments = msgContext.getAttachmentMap();

        if (attachments != null && attachments.getContentIDSet() != null && !attachments.getContentIDSet().isEmpty()) {
            // add SOAP attachments to normalized message
            Marshaller.setAttachments(attachments, jbiMsg, logger);
        }

        jbiMsg.setContent(source);

        // Get the options from the SOAP message and put them into the JBI message
        Marshaller.setProperties(msgContext, jbiMsg);
    }

    private static final Source createSourceContent(final SOAPEnvelope envelope) throws MessagingException {

        final SOAPBody body = envelope.getBody();
        final OMElement rootElement = body.getFirstElement();
        
        final OMNamespace namespace = envelope.getNamespace();
        final Iterator<OMNamespace> envNS = envelope.getAllDeclaredNamespaces();
        final Iterator<OMNamespace> bodyNS = body.getAllDeclaredNamespaces();

        if (rootElement != null) {
            rootElement.declareNamespace(namespace);
            while (envNS.hasNext()) {
                rootElement.declareNamespace((OMNamespace) envNS.next());
            }
            while (bodyNS.hasNext()) {
                rootElement.declareNamespace((OMNamespace) bodyNS.next());
            }
        
            try (final EasyByteArrayOutputStream os = new EasyByteArrayOutputStream();) {
                final XMLStreamWriter writer = XMLOutputFactories.createXMLStreamWriter(os);
                try {
                    final XMLStreamWriter encoder = new XOPEncodingStreamWriter(writer, ContentIDGenerator.DEFAULT,
                        OptimizationPolicy.ALL);
                rootElement.serialize(encoder);
                
                } finally {
                    writer.close();
                }
                return new BytesSource(os.toRawByteArray());
            } catch (final XMLStreamException e) {
                throw new MessagingException(e);
            }
        }
        return null;
    }

    /**
     * Create a the JBI payload from a Business SOAP fault.
     * 
     * @param firstDetailFaultElt
     *            the 1st element of SOAP fault details. Not {@code null}.
     * @param exchange
     *            the JBI exchange that will be filled. Not {@code null}.
     * @throws MessagingException
     *             if there is an error when creating the source content
     */
    public static void fillJBIBusinessFault(final OMElement firstDetailFaultElt, final Exchange exchange)
            throws MessagingException {

        assert firstDetailFaultElt != null;
        assert exchange != null;
        assert firstDetailFaultElt.getType() == OMNode.ELEMENT_NODE;

        final Fault fault = exchange.createFault();
        exchange.setFault(fault);

        try (final EasyByteArrayOutputStream os = new EasyByteArrayOutputStream()) {
            firstDetailFaultElt.serialize(os);
            fault.setContent(new BytesSource(os.toRawByteArray()));
        } catch (final XMLStreamException e) {
            throw new MessagingException(e);
        }
    }

    /**
     * Create a the JBI error from a Technical SOAP fault.
     * 
     * @param soapFault
     *            the SOAP fault. Not {@code null}.
     * @param exchange
     *            the JBI exchange that will be filled. Not {@code null}.
     * @throws MessagingException
     *             if there is an error when creating the source content
     */
    public static void fillJBITechnicalError(final SOAPFault soapFault, final Exchange exchange)
            throws MessagingException {

        assert soapFault != null;
        assert exchange != null;

        if (soapFault.getDetail() == null) {
            exchange.setError(new MessagingException(
                    "Technical error returned by external service provider: " + soapFault.getReason().getText()));
        } else {
            assert soapFault.getDetail().getFirstElement() == null;
            exchange.setError(new MessagingException(
                    "Technical error returned by external service provider: " + soapFault.getDetail().getText()));
        }
    }

    /**
     * <p>
     * Handles attachments. Two cases can occurs:
     * </p>
     * <ul>
     * <li>NMR attachments are declared in the XML of the NMR message using MTOM/XOP,</li>
     * <li>NMR are not declared in the XML.</li>
     * </ul>
     * <p>
     * In the first case, it is needed to replace the XML node that declares the attachment by the same using OMElement
     * (it's needed by Axis API).
     * </p>
     * <p>
     * In the other case, each attachment is added, using MTOM/XOP, in the special node inside the SOAP Body:
     * <code>&lt;soapbc:attachments xmlns:soapbc="http://petals.ow2.org/ns/soapbc"&gt;/&lt;soapbc:attachment&gt;</code>
     * </p>
     * 
     * @param nm
     * 
     * @param soapFactory
     * @param messageContext
     * @throws AxisFault
     */
    public static final void fillSOAPBodyWithAttachments(final NormalizedMessage nm,
            final SOAPFactory soapFactory, final MessageContext messageContext) throws AxisFault {
        final SOAPEnvelope env = messageContext.getEnvelope();

        if ((nm.getAttachmentNames() != null) && (!nm.getAttachmentNames().isEmpty())) {
            SOAPBody body = env.getBody();
            if (body == null) {
                body = soapFactory.createSOAPBody(env);
            }

            final OMNamespace omNs = soapFactory.createOMNamespace(SoapConstants.Component.NS_URI,
                    SoapConstants.Component.NS_PREFIX);
            OMElement rootElement = body.getFirstElement();
            if (rootElement == null) {
                rootElement = soapFactory.createOMElement("response", omNs, body);
            }

            // set property
            messageContext.setDoingMTOM(true);
            messageContext.setProperty(Constants.Configuration.ENABLE_MTOM, Constants.VALUE_TRUE);

            // Add JBI attachments to the document element
            final Set<?> names = nm.getAttachmentNames();
            for (final Object key : names) {
                final DataHandler attachment = nm.getAttachment((String) key);
                OMElement attachRefElt;
                try {
                    attachRefElt = AttachmentHelper.hasAttachmentElement(rootElement,
                            attachment, (String) key);
                    if (attachRefElt != null) {
                        // An element references the attachment, we replace it by
                        // itself using AXIOM API (It's a requirement of Axis2)
                        OMElement firstElement = attachRefElt.getFirstChildWithName(new QName(
                                "http://www.w3.org/2004/08/xop/include", "Include"));
    
                        // TODO: should we go through all the children and check
                        // the type and name?
                        if (firstElement == null) {
                            firstElement = attachRefElt.getFirstChildWithName(new QName(
                                    "http://www.w3.org/2004/08/xop/include", "include"));
                        }
    
                        // TODO: if the element is null, should we set a new
                        // attachment anyway?
                        // It seemed to work if we did not detached...
                        if (firstElement != null) {
                            firstElement.detach();
                            final OMText attach = soapFactory.createOMText(attachment, true);
                            attachRefElt.addChild(attach);
                        }
    
                        // TODO: log an error otherwise?
                    }
                } catch (UnsupportedEncodingException uee) {
                    throw new AxisFault(uee.getMessage(), FAULT_CLIENT, uee);
                }
            }
        }
    }

    /**
     * Put the SOAP attachments in the normalized message
     * 
     * @param attachments
     * @param to
     * @throws MessagingException
     */
    private static void setAttachments(final Attachments attachments, final NormalizedMessage to, final Logger logger)
            throws MessagingException {
        final String rootPartContentId = attachments.getRootPartContentID();
        for (final Object contentIdObj : attachments.getContentIDSet()) {
            final String contentId = (String) contentIdObj;
            // Avoid to copy the root part as attachment
            if (!contentId.equals(rootPartContentId)) {
                final DataHandler dh = attachments.getDataHandler(contentId);
                to.addAttachment(contentId, dh);
                if (logger.isLoggable(Level.FINE)) {
                    try {
                        logger.fine(String.format("Attachment '%s' added (size: %d bytes)", contentId,
                                dh.getInputStream().available()));
                    } catch (final IOException e) {
                        logger.log(Level.WARNING,
                                String.format(
                                        "Attachment '%s' added (size: An error occurs retrieving attachment size)",
                                        contentId),
                                e);
                    }
                }
            }
        }
    }

    /**
     * Copy properties from a SOAP message to the normalized message
     * 
     * @param from
     * @param to
     *            Normalized message
     * @throws MessagingException 
     */
    private static void setProperties(final MessageContext from, final NormalizedMessage to) throws MessagingException {

        // get the SOAP header from envelope and add it as normalized message
        // property if
        // it exists
        SOAPEnvelope env = from.getEnvelope();
        SOAPHeader header = env.getHeader();
        if (header != null) {
            final Iterator<OMElement> elements = header.getChildElements();
            final Map<String, DocumentFragment> soapHeaderElementsMap = new HashMap<String, DocumentFragment>();
            // We need to use the DocumentBuilder provided by the JVM to have a
            // DocumentFragment implementation provided by JVM, otherwise we can
            // have ClassNotFoundException on outside container because the
            // DocumentFragment implementation is not available on the other
            // side.
            final Document doc = JVMDocumentBuilders.newDocument();
            while ((elements != null) && elements.hasNext()) {
                OMElement element = elements.next();
                

                if (!element.getQName().equals(WSSE_QNAME)) {
                    try {
                        Element elt = XMLUtils.toDOM(element);

                        DocumentFragment docfrag = doc.createDocumentFragment();
                        docfrag.appendChild(doc.importNode(elt, true));
                        docfrag.normalize();

                        soapHeaderElementsMap.put(element.getQName().toString(), docfrag);
                    } catch (Exception e) {
                        throw new MessagingException(e);
                    }
                }
            }

            to.setProperty(PROTOCOL_HEADERS, soapHeaderElementsMap);
        }
    }

}
