/*
 *  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.synapse.core.axis2;

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMAttribute;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMNode;
import org.apache.axiom.soap.*;
import org.apache.axis2.AxisFault;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.synapse.SynapseException;
import org.apache.synapse.util.MessageHelper;

import javax.xml.namespace.QName;
import java.util.Iterator;
import java.util.List;

public class SOAPUtils {

    private static final Log log = LogFactory.getLog(SOAPUtils.class);

    /**
     * Converts the SOAP version of the message context.  Creates a new envelope of the given SOAP
     * version, copy headers and bodies from the old envelope and sets the new envelope to the same
     * message context.
     *
     * @param axisOutMsgCtx  messageContext where version conversion is done
     * @param soapVersionURI either org.apache.axis2.namespace.Constants.URI_SOAP12_ENV or
     *                       org.apache.axis2.namespace.Constants.URI_SOAP11_ENV
     * @throws AxisFault in case of an error in conversion
     */
    public static void convertSoapVersion(org.apache.axis2.context.MessageContext axisOutMsgCtx,
        String soapVersionURI) throws AxisFault {

        if (org.apache.axis2.namespace.Constants.URI_SOAP12_ENV.equals(soapVersionURI)) {
            convertSOAP11toSOAP12(axisOutMsgCtx);
        } else if (org.apache.axis2.namespace.Constants.URI_SOAP11_ENV.equals(soapVersionURI)) {
            convertSOAP12toSOAP11(axisOutMsgCtx);
        } else {
            throw new SynapseException("Invalid soapVersionURI:" + soapVersionURI);
        }
    }

    private static String SOAP_ATR_ACTOR = "actor";
    private static String SOAP_ATR_ROLE = "role";
    private static String SOAP_ATR_MUST_UNDERSTAND = "mustUnderstand";

    /**
     * Converts the version of the the message context to 1.2.
     * <br />
     * <b>Message Changes:</b>
     * <ol>
     *     <li>Convert envelope, header elements</li>
     *     <li>For each header block convert attribute actor to role</li>
     *     <li>For each header block convert mustUnderstand value type</li>
     *     <li>For each header block remove 1.1 namespaced other attributes</li>
     * </ol>
     *
     * <b>Fault Changes:</b>
     * <ol>
     *     <li>Convert fault element</li>
     *     <li>faultcode to Fault/Code</li>
     *     <li>faultstring to First Fault/Reason/Text with lang=en</li>
     * </ol>
     *
     * @param axisOutMsgCtx message context to be converted
     * @throws AxisFault incase conversion process fails
     */
    public static void convertSOAP11toSOAP12(
        org.apache.axis2.context.MessageContext axisOutMsgCtx) throws AxisFault {

        if(log.isDebugEnabled()) {
            log.debug("convert SOAP11 to SOAP12");
        }

        SOAPEnvelope clonedOldEnv = MessageHelper.cloneSOAPEnvelope(axisOutMsgCtx.getEnvelope());

        SOAPFactory soap12Factory = OMAbstractFactory.getSOAP12Factory();
        SOAPEnvelope newEnvelope  = soap12Factory.getDefaultEnvelope();

        if (clonedOldEnv.getHeader() != null) {
            Iterator itr = clonedOldEnv.getHeader().getChildren();
            while (itr.hasNext()) {
                OMNode omNode = (OMNode) itr.next();

                if (omNode instanceof SOAPHeaderBlock) {
                    SOAPHeaderBlock soapHeader = (SOAPHeaderBlock) omNode;
                    SOAPHeaderBlock newSOAPHeader = soap12Factory.createSOAPHeaderBlock(
                        soapHeader.getLocalName(), soapHeader.getNamespace());
                    Iterator allAttributes = soapHeader.getAllAttributes();
                    boolean hasAttributes = false;
                    while(allAttributes.hasNext()) {
                        hasAttributes = true;
                        OMAttribute attr = (OMAttribute) allAttributes.next();
                        if(attr.getNamespace() != null
                            && SOAP11Constants.SOAP_ENVELOPE_NAMESPACE_URI.equals(
                            attr.getNamespace().getNamespaceURI())) {
                            String attrName = attr.getLocalName();

                            if(SOAP_ATR_ACTOR.equals(attrName)) {
                                OMAttribute newAtr = omNode.getOMFactory().createOMAttribute(
                                    SOAP_ATR_ROLE, newEnvelope.getNamespace(),
                                    attr.getAttributeValue());
                                newSOAPHeader.addAttribute(newAtr);

                            } else if(SOAP_ATR_MUST_UNDERSTAND.equals(attrName)) {
                                boolean isMustUnderstand = soapHeader.getMustUnderstand();
                                newSOAPHeader.setMustUnderstand(isMustUnderstand);

                            } else {
                                log.warn("removed unsupported attribute from SOAP 1.1 " +
                                    "namespace when converting to SOAP 1.2:" + attrName);
                            }

                        } else {
                            newSOAPHeader.addAttribute(attr);
                        }

                        Iterator itrChildren = soapHeader.getChildren();
                        while (itrChildren.hasNext()) {
                            OMNode node = (OMNode) itrChildren.next();
                            itrChildren.remove();
                            newSOAPHeader.addChild(node);
                        }

                        newEnvelope.getHeader().addChild(newSOAPHeader);
                    }
                    // This block will execute if the header block doesn't have any attribute
                    if (!hasAttributes) {
                        Iterator itrChildren = soapHeader.getChildren();
                        while (itrChildren.hasNext()) {
                            OMNode node = (OMNode) itrChildren.next();
                            itrChildren.remove();
                            newSOAPHeader.addChild(node);
                        }

                        newEnvelope.getHeader().addChild(newSOAPHeader);
                    }
                } else {
                    itr.remove();
                    newEnvelope.getHeader().addChild(omNode);
                }

            } // while (itr.hasNext())

        } // if (clonedOldEnv.getHeader() != null)

        if (clonedOldEnv.getBody() != null) {

            Iterator itrBodyChildren = clonedOldEnv.getBody().getChildren();
            while (itrBodyChildren.hasNext()) {
                OMNode omNode = (OMNode) itrBodyChildren.next();

                if (omNode != null && omNode instanceof SOAPFault) {

                    SOAPFault soapFault = (SOAPFault) omNode;
                    SOAPFault newSOAPFault = soap12Factory.createSOAPFault();
                    newEnvelope.getBody().addChild(newSOAPFault);
                    // get the existing envelope
                    SOAPFaultCode code = soapFault.getCode();
                    if(code != null) {
                        SOAPFaultCode newSOAPFaultCode = soap12Factory.createSOAPFaultCode();
                        newSOAPFault.setCode(newSOAPFaultCode);

                        QName s11Code = code.getTextAsQName();
                        if (s11Code != null) {
                            // get the corresponding SOAP12 fault code
                            // for the provided SOAP11 fault code
                            SOAPFaultValue newSOAPFaultValue
                                    = soap12Factory.createSOAPFaultValue(newSOAPFaultCode);
                            newSOAPFaultValue.setText(getMappingSOAP12Code(s11Code));
                        }

                    }

                    SOAPFaultReason reason = soapFault.getReason();
                    if(reason != null) {
                        SOAPFaultReason newSOAPFaultReason
                                = soap12Factory.createSOAPFaultReason(newSOAPFault);
                        String reasonText = reason.getText();
                        if(reasonText != null) {
                            SOAPFaultText newSOAPFaultText
                                    = soap12Factory.createSOAPFaultText(newSOAPFaultReason);
                            newSOAPFaultText.setLang("en"); // hard coded
                            newSOAPFaultText.setText(reasonText);
                        }
                        newSOAPFault.setReason(newSOAPFaultReason);
                    }

                    SOAPFaultDetail detail = soapFault.getDetail();
                    if(detail != null) {
                        SOAPFaultDetail newSOAPFaultDetail
                                = soap12Factory.createSOAPFaultDetail(newSOAPFault);
                        Iterator<OMElement> iter = detail.getAllDetailEntries();
                        while (iter.hasNext()) {
                            OMElement detailEntry = iter.next();
                            iter.remove();
                            newSOAPFaultDetail.addDetailEntry(detailEntry);
                        }
                         newSOAPFault.setDetail(newSOAPFaultDetail);
                    }

                } else {
                    itrBodyChildren.remove();
                    newEnvelope.getBody().addChild(omNode);

                } // if (omNode instanceof SOAPFault)

            } // while (itrBodyChildren.hasNext())

        } //if (clonedOldEnv.getBody() != null)

        axisOutMsgCtx.setEnvelope(newEnvelope);
    }

    /**
     * Converts the version of the the message context to 1.1.
     * <br />
     * <b>Message Changes:</b>
     * <ol>
     *     <li>Convert envelope, header elements</li>
     *     <li>For each header block convert attribute role to actor</li>
     *     <li>For each header block convert mustUnderstand value type</li>
     *     <li>For each header block remove 1.2 namespaced other attributes</li>
     * </ol>
     *
     * <b>Fault Changes:</b>
     * <ol>
     *     <li>Convert fault element</li>
     *     <li>Fault/Code to faultcode</li>
     *     <li>First Fault/Reason/Text to faultstring</li>
     * </ol>
     * @param axisOutMsgCtx message context to be converted
     * @throws AxisFault in case of an error in conversion
     */
    public static void convertSOAP12toSOAP11(
        org.apache.axis2.context.MessageContext axisOutMsgCtx) throws AxisFault {

        if (log.isDebugEnabled()) {
            log.debug("convert SOAP12 to SOAP11");
        }

        SOAPEnvelope clonedOldEnv = MessageHelper.cloneSOAPEnvelope(axisOutMsgCtx.getEnvelope());

        SOAPFactory soap11Factory = OMAbstractFactory.getSOAP11Factory();
        SOAPEnvelope newEnvelope  = soap11Factory.getDefaultEnvelope();
        if (clonedOldEnv.getHeader() != null) {
            Iterator itr = clonedOldEnv.getHeader().getChildren();
            while (itr.hasNext()) {
                OMNode omNode = (OMNode) itr.next();

                if (omNode instanceof SOAPHeaderBlock) {
                    SOAPHeaderBlock soapHeaderBlock = (SOAPHeaderBlock) omNode;
                    SOAPHeaderBlock newSOAPHeader = soap11Factory.createSOAPHeaderBlock(
                        soapHeaderBlock.getLocalName(), soapHeaderBlock.getNamespace());

                    Iterator allAttributes = soapHeaderBlock.getAllAttributes();
                    boolean hasAttributes = false;
                    while(allAttributes.hasNext()) {
                        hasAttributes = true;
                        OMAttribute attr = (OMAttribute) allAttributes.next();
                        if (attr.getNamespace() != null
                            && SOAP12Constants.SOAP_ENVELOPE_NAMESPACE_URI.equals(
                            attr.getNamespace().getNamespaceURI())) {
                            String attrName = attr.getLocalName();

                            if (SOAP_ATR_ROLE.equals(attrName)) {
                                OMAttribute newAtr = omNode.getOMFactory().createOMAttribute(
                                    SOAP_ATR_ACTOR, newEnvelope.getNamespace(),
                                    attr.getAttributeValue());
                                newSOAPHeader.addAttribute(newAtr);

                            } else if(SOAP_ATR_MUST_UNDERSTAND.equals(attrName)) {
                                boolean isMustUnderstand = soapHeaderBlock.getMustUnderstand();
                                newSOAPHeader.setMustUnderstand(isMustUnderstand);

                            } else {
                                log.warn("removed unsupported attribute from SOAP 1.2 " +
                                    "namespace when converting to SOAP 1.1:" + attrName);
                            }

                        } else {
                            newSOAPHeader.addAttribute(attr);
                        }

                        Iterator itrChildren = soapHeaderBlock.getChildren();
                        while (itrChildren.hasNext()) {
                            OMNode node = (OMNode) itrChildren.next();
                            itrChildren.remove();
                            newSOAPHeader.addChild(node);
                        }

                        newEnvelope.getHeader().addChild(newSOAPHeader);
                    }
                    // This block will execute if the header block doesn't have any attribute
                    if (!hasAttributes) {
                        Iterator itrChildren = soapHeaderBlock.getChildren();
                        while (itrChildren.hasNext()) {
                            OMNode node = (OMNode) itrChildren.next();
                            itrChildren.remove();
                            newSOAPHeader.addChild(node);
                        }
                    }
                    newEnvelope.getHeader().addChild(newSOAPHeader);
                } else {
                    itr.remove();
                    newEnvelope.getHeader().addChild(omNode);
                }
            }
        }

        if (clonedOldEnv.getBody() != null) {
            if (clonedOldEnv.hasFault()) {
                SOAPFault soapFault = clonedOldEnv.getBody().getFault();
                SOAPFault newSOAPFault = soap11Factory.createSOAPFault();
                newEnvelope.getBody().addChild(newSOAPFault);

                SOAPFaultCode code = soapFault.getCode();
                if(code != null) {
                    SOAPFaultCode newSOAPFaultCode
                            = soap11Factory.createSOAPFaultCode(newSOAPFault);

                    SOAPFaultValue value = code.getValue();
                    if(value != null) {
                        // get the corresponding SOAP12 fault code
                        // for the provided SOAP11 fault code
                        soap11Factory.createSOAPFaultValue(newSOAPFaultCode);
                        if(value.getTextAsQName() != null) {
                            newSOAPFaultCode.setText(
                                    getMappingSOAP11Code(value.getTextAsQName()));
                        }
                    }
                }

                SOAPFaultReason reason = soapFault.getReason();
                if(reason != null) {
                    SOAPFaultReason newSOAPFaultReason
                            = soap11Factory.createSOAPFaultReason(newSOAPFault);

                    List allSoapTexts = reason.getAllSoapTexts();
                    Iterator iterAllSoapTexts = allSoapTexts.iterator();
                    if (iterAllSoapTexts.hasNext()) {
                        SOAPFaultText soapFaultText = (SOAPFaultText) iterAllSoapTexts.next();
                        iterAllSoapTexts.remove();
                        newSOAPFaultReason.setText(soapFaultText.getText());
                    }
                }

                SOAPFaultDetail detail = soapFault.getDetail();
                if(detail != null) {
                    SOAPFaultDetail newSOAPFaultDetail
                            = soap11Factory.createSOAPFaultDetail(newSOAPFault);
                    Iterator<OMElement> iter = detail.getAllDetailEntries();
                    while (iter.hasNext()) {
                        OMElement detailEntry = iter.next();
                        iter.remove();
                        newSOAPFaultDetail.addDetailEntry(detailEntry);
                    }
                    newSOAPFault.setDetail(newSOAPFaultDetail);
                }

            } else {
                Iterator itr = clonedOldEnv.getBody().getChildren();
                while (itr.hasNext()) {
                    OMNode omNode = (OMNode) itr.next();
                    if (omNode != null) {
                        itr.remove();
                        newEnvelope.getBody().addChild(omNode);
                    }
                }
            }

        }
        
        axisOutMsgCtx.setEnvelope(newEnvelope);
    }

    /**********************************************************************
     *                     Fault code conversions                         *
     **********************************************************************/

    private static final QName S11_FAULTCODE_VERSIONMISMATCH = new QName(
            SOAP11Constants.SOAP_ENVELOPE_NAMESPACE_URI, "VersionMismatch",
            SOAP11Constants.SOAP_DEFAULT_NAMESPACE_PREFIX);
    private static final QName S12_FAULTCODE_VERSIONMISMATCH = new QName(
            SOAP12Constants.SOAP_ENVELOPE_NAMESPACE_URI, "VersionMismatch",
            SOAP12Constants.SOAP_DEFAULT_NAMESPACE_PREFIX);

    private static final QName S11_FAULTCODE_MUSTUNDERSTAND = new QName(
            SOAP11Constants.SOAP_ENVELOPE_NAMESPACE_URI, "MustUnderstand",
            SOAP11Constants.SOAP_DEFAULT_NAMESPACE_PREFIX);
    private static final QName S12_FAULTCODE_MUSTUNDERSTAND = new QName(
            SOAP12Constants.SOAP_ENVELOPE_NAMESPACE_URI, "MustUnderstand",
            SOAP12Constants.SOAP_DEFAULT_NAMESPACE_PREFIX);

    private static final QName S11_FAULTCODE_CLIENT = new QName(
            SOAP11Constants.SOAP_ENVELOPE_NAMESPACE_URI, "Client",
            SOAP11Constants.SOAP_DEFAULT_NAMESPACE_PREFIX);
    private static final QName S12_FAULTCODE_SENDER = new QName(
            SOAP12Constants.SOAP_ENVELOPE_NAMESPACE_URI, "Sender",
            SOAP12Constants.SOAP_DEFAULT_NAMESPACE_PREFIX);

    private static final QName S11_FAULTCODE_SERVER = new QName(
            SOAP11Constants.SOAP_ENVELOPE_NAMESPACE_URI, "Server",
            SOAP11Constants.SOAP_DEFAULT_NAMESPACE_PREFIX);
    private static final QName S12_FAULTCODE_RECEIVER = new QName(
            SOAP12Constants.SOAP_ENVELOPE_NAMESPACE_URI, "Receiver",
            SOAP12Constants.SOAP_DEFAULT_NAMESPACE_PREFIX);

    private static final QName S12_FAULTCODE_DATAENCODINGUNKNOWN = new QName(
            SOAP12Constants.SOAP_ENVELOPE_NAMESPACE_URI, "DataEncodingUnknown",
            SOAP12Constants.SOAP_DEFAULT_NAMESPACE_PREFIX);

    private static QName getMappingSOAP12Code(QName soap11Code) {
        if (S11_FAULTCODE_VERSIONMISMATCH.equals(soap11Code)) {
            return S12_FAULTCODE_VERSIONMISMATCH;
        } else if (S11_FAULTCODE_MUSTUNDERSTAND.equals(soap11Code)) {
            return S12_FAULTCODE_MUSTUNDERSTAND;
        } else if (S11_FAULTCODE_CLIENT.equals(soap11Code)) {
            return S12_FAULTCODE_SENDER;
        } else if (S11_FAULTCODE_SERVER.equals(soap11Code)) {
            return S12_FAULTCODE_RECEIVER;
        } else {
            log.warn("An unidentified SOAP11 FaultCode encountered, returning a blank QName");
            return new QName("", "");
        }
    }

    private static QName getMappingSOAP11Code(QName soap12Code) {
        if (S12_FAULTCODE_VERSIONMISMATCH.equals(soap12Code)) {
            return S11_FAULTCODE_VERSIONMISMATCH;
        } else if (S12_FAULTCODE_MUSTUNDERSTAND.equals(soap12Code)) {
            return S11_FAULTCODE_MUSTUNDERSTAND;
        } else if (S12_FAULTCODE_SENDER.equals(soap12Code)) {
            return S11_FAULTCODE_SERVER;
        } else if (S12_FAULTCODE_RECEIVER.equals(soap12Code)) {
            return S11_FAULTCODE_SERVER;
        } else if (S12_FAULTCODE_DATAENCODINGUNKNOWN.equals(soap12Code)) {
            log.debug("There is no matching SOAP11 code value for SOAP12 fault code " +
                    "DataEncodingUnknown, returning a blank QName");
            return new QName("");
        } else {
            log.warn("An unidentified SOAP11 FaultCode encountered, returning a blank QName");
            return new QName("");
        }
    }
}
