package org.apache.synapse.util;

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.OMNamespace;
import org.apache.axiom.om.OMNode;
import org.apache.axiom.om.util.ElementHelper;
import org.apache.axiom.soap.SOAP11Constants;
import org.apache.axiom.soap.SOAPEnvelope;
import org.apache.axiom.soap.SOAPFactory;
import org.apache.axiom.soap.SOAPFault;
import org.apache.axiom.soap.SOAPFaultCode;
import org.apache.axiom.soap.SOAPFaultDetail;
import org.apache.axiom.soap.SOAPFaultNode;
import org.apache.axiom.soap.SOAPFaultReason;
import org.apache.axiom.soap.SOAPFaultRole;
import org.apache.axiom.soap.SOAPFaultText;
import org.apache.axiom.soap.SOAPFaultValue;
import org.apache.axiom.soap.SOAPHeader;
import org.apache.axiom.soap.SOAPHeaderBlock;
import org.apache.axiom.util.UIDGenerator;
import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
import org.apache.axis2.addressing.AddressingConstants;
import org.apache.axis2.client.Options;
import org.apache.axis2.transport.http.HTTPConstants;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.neethi.Policy;
import org.apache.neethi.PolicyEngine;
import org.apache.synapse.ContinuationState;
import org.apache.synapse.FaultHandler;
import org.apache.synapse.MessageContext;
import org.apache.synapse.SynapseConstants;
import org.apache.synapse.SynapseException;
import org.apache.synapse.commons.json.JsonUtil;
import org.apache.synapse.commons.CorrelationConstants;
import org.apache.synapse.continuation.ContinuationStackManager;
import org.apache.synapse.continuation.SeqContinuationState;
import org.apache.synapse.core.axis2.Axis2MessageContext;
import org.apache.synapse.core.axis2.ResponseState;
import org.apache.synapse.debug.constructs.SynapseMediationFlowPoint;
import org.apache.synapse.mediators.eip.EIPConstants;
import org.apache.synapse.mediators.template.TemplateContext;
import org.apache.synapse.transport.util.MessageHandlerProvider;
import org.apache.synapse.transport.http.conn.SynapseDebugInfoHolder;
import org.apache.synapse.transport.passthru.PassThroughConstants;

import javax.mail.internet.ContentType;
import javax.mail.internet.ParseException;
import javax.xml.stream.XMLStreamException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.UUID;

import static org.apache.synapse.SynapseConstants.PASSWORD_PATTERN;
import static org.apache.synapse.SynapseConstants.URL_PATTERN;

/**
 *
 */
public class MessageHelper {


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

    public static MessageContext cloneMessageContext(MessageContext synCtx, boolean cloneSoapEnvelope) throws AxisFault {
        return cloneMessageContext(synCtx, cloneSoapEnvelope, true);
    }

    /**
     * This method is similar to {@link MessageHelper#cloneMessageContext(MessageContext, boolean, boolean, boolean, boolean)}
     * In order to refactor the code, new method signature was created and this method calls the new method
     *
     * @param synCtx Synapse MessageContext which has to be cloned
     * @param cloneSoapEnvelope The flag to say whether to clone the SOAP envelope or not.
     * @param isCloneJson The flag to say whether to clone the JSON payload or not.
     * @return cloned Synapse MessageContext.
     * @throws AxisFault if there is a failure in creating the new Synapse MC or in a failure in
     *                  cloning the underlying axis2 MessageContext.
     */
    public static MessageContext cloneMessageContext(MessageContext synCtx, boolean cloneSoapEnvelope,
                                                     boolean isCloneJson) throws AxisFault {
        return cloneMessageContext(synCtx, cloneSoapEnvelope, isCloneJson, false, true);
    }

    /**
     * This method is similar to {@link MessageHelper#cloneMessageContext(MessageContext, boolean, boolean, boolean, boolean)}
     * other than always generating seperate correlation ids for the cloned message contexts
     *
     * @param synCtx Synapse MessageContext which has to be cloned
     * @param cloneSoapEnvelope The flag to say whether to clone the SOAP envelope or not.
     * @param isCloneJson The flag to say whether to clone the JSON payload or not.
     * @param isAggregate whether this method is called inside aggregate mediator
     * @return cloned Synapse MessageContext.
     * @throws AxisFault if there is a failure in creating the new Synapse MC or in a failure in
     *                  cloning the underlying axis2 MessageContext.
     */
    public static MessageContext cloneMessageContext(MessageContext synCtx, boolean cloneSoapEnvelope,
                                                     boolean isCloneJson, boolean isAggregate) throws AxisFault {
        return cloneMessageContext(synCtx, cloneSoapEnvelope, isCloneJson, isAggregate, true);
    }

    /**
     * This method will simulate cloning the message context and creating an exact copy of the
     * passed message. One should use this method with care; that is because, inside the new MC,
     * most of the attributes of the MC like opCtx and so on are still kept as references inside
     * the axis2 MessageContext for performance improvements. (Note: U dont have to worrie
     * about the SOAPEnvelope, it is a cloned copy and not a reference from any other MC)
     * @param synCtx - this will be cloned.
     * @param cloneSoapEnvelope whether to clone the soap envelope.
     * @param isCloneJson whether to clone the JSON payload.
     * @param isAggregate whether this method is called inside aggregate mediator
     * @param isCloneCorrelationId whether to generate correlation id for the cloned message
     * @return cloned Synapse MessageContext.
     * @throws AxisFault if there is a failure in creating the new Synapse MC or in a failure in
     *                   clonning the underlying axis2 MessageContext.
     * @see MessageHelper#cloneAxis2MessageContext
     */
    public static MessageContext cloneMessageContext(MessageContext synCtx, boolean cloneSoapEnvelope,
                                                     boolean isCloneJson, boolean isAggregate, boolean isCloneCorrelationId) throws AxisFault {

        // creates the new MessageContext and clone the internal axis2 MessageContext
        // inside the synapse message context and place that in the new one
        MessageContext newCtx = synCtx.getEnvironment().createMessageContext();
        Axis2MessageContext axis2MC = (Axis2MessageContext) newCtx;
        axis2MC.setAxis2MessageContext(
                cloneAxis2MessageContext(((Axis2MessageContext) synCtx).getAxis2MessageContext(),
                        cloneSoapEnvelope, isCloneJson, isAggregate));

        newCtx.setConfiguration(synCtx.getConfiguration());
        newCtx.setEnvironment(synCtx.getEnvironment());
        newCtx.setContextEntries(synCtx.getContextEntries());

        //Correlation logging code starts here
        org.apache.axis2.context.MessageContext originalAxis2Ctx =
                ((Axis2MessageContext) synCtx).getAxis2MessageContext();
        if (originalAxis2Ctx.getProperty(CorrelationConstants.CORRELATION_ID) != null  && isCloneCorrelationId) {
            String originalCorrelationId = originalAxis2Ctx.getProperty(CorrelationConstants.CORRELATION_ID).toString();
            axis2MC.getAxis2MessageContext().setProperty(CorrelationConstants.CORRELATION_ID, originalCorrelationId
                    + "_" + UUID.randomUUID().toString());
        }
        //Correlation logging code ends here

        // set the parent correlation details to the cloned MC -
        //                              for the use of aggregation like tasks
        newCtx.setProperty(EIPConstants.AGGREGATE_CORRELATION, synCtx.getMessageID());

        // copying the core parameters of the synapse MC
        newCtx.setTo(synCtx.getTo());
        newCtx.setReplyTo(synCtx.getReplyTo());
        newCtx.setSoapAction(synCtx.getSoapAction());
        newCtx.setWSAAction(synCtx.getWSAAction());
        newCtx.setResponse(synCtx.isResponse());

        // copy all the synapse level properties to the newCtx
        for (Object o : synCtx.getPropertyKeySet()) {
            // If there are non String keyed properties neglect them rather than trow exception
            if (o instanceof String) {
                String strkey = (String) o;
                Object obj = synCtx.getProperty(strkey);
                if (obj instanceof String) {
                    // No need to do anything since Strings are immutable
                } else if (obj instanceof ArrayList) {
                    if (log.isDebugEnabled()) {
                        log.debug("Deep clone Started for  ArrayList property: " + strkey + ".");
                    }
                    // Call this method to deep clone ArrayList
                    obj = cloneArrayList((ArrayList) obj);
                    if (log.isDebugEnabled()) {
                        log.debug("Deep clone Ended for  ArrayList property: " + strkey + ".");
                    }
                } else if (obj instanceof Stack
                           && strkey.equals(SynapseConstants.SYNAPSE__FUNCTION__STACK)) {
                    if (log.isDebugEnabled()) {
                        log.debug("Deep clone for Template function stack");
                    }
                    obj = getClonedTemplateStack((Stack<TemplateContext>) obj);
                } else if (obj instanceof OMElement) {
                    if (log.isDebugEnabled()) {
                        log.debug("Deep clone for OMElement");
                    }
                    obj = (OMElement) ((OMElement) obj).cloneOMElement();
                } else if (obj instanceof ResponseState) {
                    // do nothing and let the same reference to go to the cloned context
                } else{
                    /**
                     * Need to add conditions according to type if found in
                     * future
                     */
                    if (log.isDebugEnabled()) {
                        log.warn("Deep clone not happened for property : " + strkey +
                                 ". Class type : " + obj.getClass().getName());
                    }
                }
                newCtx.setProperty(strkey, obj);
            }
        }

        // copy all the variables to the newCtx
        for (Object o : synCtx.getVariableKeySet()) {
            String strkey = (String) o;
            Object obj = synCtx.getVariable(strkey);
            if (obj instanceof OMElement) {
                if (log.isDebugEnabled()) {
                    log.debug("Deep clone for OMElement");
                }
                obj = ((OMElement) obj).cloneOMElement();
            }
            newCtx.setVariable(strkey, obj);
        }

        // Make deep copy of fault stack so that parent will not be lost it's fault stack
        Stack<FaultHandler> faultStack = synCtx.getFaultStack();
        if (!faultStack.isEmpty()) {

            List<FaultHandler> newFaultStack = new ArrayList<FaultHandler>();
            newFaultStack.addAll(faultStack);

            for (FaultHandler faultHandler : newFaultStack) {
                if (faultHandler != null) {
                    newCtx.pushFaultHandler(faultHandler);
                }
            }
        }

		Stack<TemplateContext> functionStack =
		                                       (Stack) synCtx.getProperty(SynapseConstants.SYNAPSE__FUNCTION__STACK);
		if (functionStack != null) {
			newCtx.setProperty(SynapseConstants.SYNAPSE__FUNCTION__STACK, functionStack.clone());
		}

        if (log.isDebugEnabled()) {
            log.info("Parent's Fault Stack : " + faultStack
                     + " : Child's Fault Stack :" + newCtx.getFaultStack());
        }

        // Copy ContinuationStateStack from original MC to the new MC
        if (synCtx.isContinuationEnabled()) {
            Stack<ContinuationState> continuationStates =  synCtx.getContinuationStateStack();
            newCtx.setContinuationEnabled(true);

            for (ContinuationState continuationState : continuationStates) {
                if (continuationState != null) {
                   newCtx.pushContinuationState(
                           ContinuationStackManager.getClonedSeqContinuationState(
                                   (SeqContinuationState) continuationState));
                }
            }
        }
        newCtx.setMessageFlowTracingState(synCtx.getMessageFlowTracingState());
        return newCtx;
    }

    /**
     * This method does exactly what {@link MessageHelper#cloneMessageContext(MessageContext)} does,
     * other than cloning the SOAP envelop based on the {@code cloneSOAPEnvelope} argument.
     * @param synCtx Synapse message context to be cloned.
     * @return The cloned Synapse Message Context.
     * @throws AxisFault If something goes wrong with message cloning.
     */
    public static MessageContext cloneMessageContext(MessageContext synCtx) throws AxisFault {

        return cloneMessageContext(synCtx, true);
    }

    /**
     * In an effort to refactor the code, this method is deprecated.
     * Use {@link #cloneMessageContext(MessageContext, boolean, boolean, boolean, boolean)}
     * with the isAggregate flag set to true
     * @param synCtx Synapse message context to be cloned.
     * @return The cloned Synapse Message Context.
     * @throws AxisFault If something goes wrong with message cloning.
     */
    @Deprecated
    public static MessageContext cloneMessageContextForAggregateMediator(MessageContext synCtx) throws AxisFault {
        return cloneMessageContext(synCtx, true, false, true, true);
    }

    /**
     * Get a clone of a Template Function stack
     *
     * @param oriTemplateStack original template function stack to be cloned
     * @return clone of a Template Function stack
     */
    public static Stack<TemplateContext> getClonedTemplateStack(
            Stack<TemplateContext> oriTemplateStack) {

        Stack<TemplateContext> clonedTemplateStack = new Stack<TemplateContext>();

        for (TemplateContext oriTemplateCtx : oriTemplateStack) {
            TemplateContext clonedTemplateCtx =
                    new TemplateContext(oriTemplateCtx.getName(), oriTemplateCtx.getParameters());
            Map oriValueMap = oriTemplateCtx.getMappedValues();
            Map clonedValueMap = new HashMap();

            for (Object key : oriValueMap.keySet()) {
                Object value = oriValueMap.get(key);

                if (value instanceof ArrayList) {
                    value = cloneArrayList((ArrayList<Object>) value);
                }

                clonedValueMap.put(key, value);
            }
            clonedTemplateCtx.setMappedValues(clonedValueMap);
            clonedTemplateStack.push(clonedTemplateCtx);
        }

        return clonedTemplateStack;
    }

     /*
     * This method will deep clone array list by creating a new ArrayList and cloning and adding each element in it
     * */
	public static ArrayList<Object> cloneArrayList(ArrayList<Object> arrayList) {
		ArrayList<Object> newArrayList = null;
		if (arrayList != null) {
			newArrayList = new ArrayList<Object>();
			for (Object obj : arrayList) {
				if (obj instanceof SOAPHeaderBlock) {
					SOAPFactory fac = (SOAPFactory) ((SOAPHeaderBlock) obj).getOMFactory();
					obj = ((SOAPHeaderBlock) obj).cloneOMElement();
					try {
						obj = ElementHelper.toSOAPHeaderBlock((OMElement) obj, fac);
					} catch (Exception e) {
						handleException(e);
					}
				} else if (obj instanceof SOAPEnvelope) {
					SOAPEnvelope enve = (SOAPEnvelope) obj;
					obj = MessageHelper.cloneSOAPEnvelope(enve);
				} else if (obj instanceof OMElement) {
					obj = ((OMElement) obj).cloneOMElement();
				} else {
					if (log.isDebugEnabled()) {
						log.error("Array List deep clone not implemented for Class type : " +
						          obj.getClass().getName());
					}
				}
				newArrayList.add(obj);
			}
		}
		return newArrayList;
	}

    public static org.apache.axis2.context.MessageContext cloneAxis2MessageContext(
            org.apache.axis2.context.MessageContext mc, boolean cloneSoapEnvelope) throws AxisFault {
        return cloneAxis2MessageContext(mc, cloneSoapEnvelope, true);
    }

    /**
     * This method is similar to {@link MessageHelper#cloneMessageContext(MessageContext, boolean, boolean, boolean, boolean)}
     * In order to refactor the code, new method signature was created and this method calls the new method
     *
     * @param mc Axis2 message context
     * @param cloneSoapEnvelope The flag to say whether to clone the SOAP envelope or not.
     * @param isCloneJson The flag to say whether to clone the JSON payload or not.
     * @return The cloned Axis2 message context.
     * @throws AxisFault If something goes wrong during cloning.
     */
    public static org.apache.axis2.context.MessageContext cloneAxis2MessageContext(
            org.apache.axis2.context.MessageContext mc, boolean cloneSoapEnvelope, boolean isCloneJson)
            throws AxisFault {
	    return cloneAxis2MessageContext(mc, cloneSoapEnvelope, isCloneJson, false);
    }

    /**
     * This method will simulate cloning the message context and creating an exact copy of the
     * passed message. One should use this method with care; that is because, inside the new MC,
     * most of the attributes of the MC like opCtx and so on are still kept as references. Otherwise
     * there will be perf issues. But ..... this may reveal in some conflicts in the cloned message
     * if you try to do advanced mediations with the cloned message, in which case you should
     * manually get a clone of the changing part of the MC and set that cloned part to your MC.
     * Changing the MC after doing that will solve most of the issues. (Note: You don't have to worry
     * about the SOAPEnvelope, it is a cloned copy and not a reference from any other MC)
     *
     * @param mc - this will be cloned for getting an exact copy
     * @param cloneSoapEnvelope The flag to say whether to clone the SOAP envelope or not.
     * @param isCloneJson The flag to say whether to clone the JSON payload or not.
     * @param isAggregrate The flag to say whether it is called inside aggregate mediator.
     * @return cloned MessageContext from the given mc
     * @throws AxisFault if there is a failure in copying the certain attributes of the
     *          provided message context
     */
    public static org.apache.axis2.context.MessageContext cloneAxis2MessageContext(
        org.apache.axis2.context.MessageContext mc, boolean cloneSoapEnvelope, boolean isCloneJson,
        boolean isAggregrate) throws AxisFault {

        //building the message payload since buffer can not be cloned. otherwise cloned message will have
        //empty buffer in PASS_THROUGH_PIPE without the message payload.
        try {
            MessageHandlerProvider.getMessageHandler(mc).buildMessage(mc, false);
        } catch (IOException e) {
            handleException(e);
        } catch (XMLStreamException e) {
            handleException(e);
        }

        org.apache.axis2.context.MessageContext newMC = clonePartially(mc, isAggregrate);
        if (cloneSoapEnvelope) {
            newMC.setEnvelope(cloneSOAPEnvelope(mc.getEnvelope()));
        }
        // XXX: always this section must come after the above step. ie. after applying Envelope.
        // That is to get the existing headers into the new envelope.
        if (isCloneJson) {
            JsonUtil.cloneJsonPayload(mc, newMC);
        }
        newMC.setOptions(cloneOptions(mc.getOptions()));

        newMC.setServiceContext(mc.getServiceContext());
        newMC.setOperationContext(mc.getOperationContext());
        newMC.setAxisMessage(mc.getAxisMessage());
        if (newMC.getAxisMessage() != null) {
            newMC.getAxisMessage().setParent(mc.getAxisOperation());
        }
        newMC.setAxisService(mc.getAxisService());

        // copying transport related parts from the original
        newMC.setTransportIn(mc.getTransportIn());
        newMC.setTransportOut(mc.getTransportOut());
        newMC.setProperty(org.apache.axis2.Constants.OUT_TRANSPORT_INFO,
            mc.getProperty(org.apache.axis2.Constants.OUT_TRANSPORT_INFO));

        newMC.setProperty(org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS,
            getClonedTransportHeaders(mc));
        newMC.removeProperty(PassThroughConstants.PASS_THROUGH_PIPE);
        return newMC;
    }

    /**
     * This method does exactly what {@link MessageHelper#cloneAxis2MessageContext(org.apache.axis2.context.MessageContext)} does,
     * other than cloning the SOAP envelop based on the {@code cloneSOAPEnvelope} argument.
     *
     * @param mc Axis2 message context
     * @return The cloned Axis2 message context.
     * @throws AxisFault If something goes wrong during cloning.
     */
    public static org.apache.axis2.context.MessageContext cloneAxis2MessageContext(
               org.apache.axis2.context.MessageContext mc) throws AxisFault {
        return cloneAxis2MessageContext(mc, true);
    }

    /**
     * In an effort to refactor the code, this method is deprecated.
     * Use {@link #cloneAxis2MessageContext(org.apache.axis2.context.MessageContext, boolean, boolean, boolean)}
     * with the isAggregate flag set to true
     * @param mc Axis2 message context
     * @return The cloned Axis2 message context.
     * @throws AxisFault If something goes wrong during cloning.
     */
    @Deprecated
    private static org.apache.axis2.context.MessageContext cloneAxis2MessageContextForAggregate(
			org.apache.axis2.context.MessageContext mc) throws AxisFault {
        return cloneAxis2MessageContext(mc, true, false, true);
	}


    public static Map getClonedTransportHeaders(org.apache.axis2.context.MessageContext msgCtx) {

        Map headers = (Map) msgCtx.
                getProperty(org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS);
        Map<String, Object> clonedHeaders;
        if (headers instanceof TreeMap) {
            clonedHeaders = new TreeMap<String, Object>(new Comparator<String>() {
                public int compare(String s1, String s2) {
                    return s1.compareToIgnoreCase(s2);
                }
            });
        } else {
            clonedHeaders = new HashMap<String, Object>();
        }

        if (headers != null && !headers.isEmpty()) {
            for (Object o : headers.keySet()) {
                String headerName = (String) o;
                clonedHeaders.put(headerName, headers.get(headerName));
            }
        }

        return clonedHeaders;
    }

    public static org.apache.axis2.context.MessageContext clonePartially(
            org.apache.axis2.context.MessageContext ori) throws AxisFault {
        return clonePartially(ori, false);
    }

    public static org.apache.axis2.context.MessageContext clonePartially(
        org.apache.axis2.context.MessageContext ori, boolean isAggregate) throws AxisFault {

        org.apache.axis2.context.MessageContext newMC
            = new org.apache.axis2.context.MessageContext();

        // do not copy options from the original
        newMC.setConfigurationContext(ori.getConfigurationContext());
        newMC.setMessageID(UIDGenerator.generateURNString());
        newMC.setTo(ori.getTo());
        newMC.setSoapAction(ori.getSoapAction());

        newMC.setProperty(org.apache.axis2.Constants.Configuration.CHARACTER_SET_ENCODING,
                ori.getProperty(org.apache.axis2.Constants.Configuration.CHARACTER_SET_ENCODING));
        newMC.setProperty(org.apache.axis2.Constants.Configuration.ENABLE_MTOM,
                ori.getProperty(org.apache.axis2.Constants.Configuration.ENABLE_MTOM));
        newMC.setProperty(org.apache.axis2.Constants.Configuration.ENABLE_SWA,
                ori.getProperty(org.apache.axis2.Constants.Configuration.ENABLE_SWA));
        newMC.setProperty(Constants.Configuration.HTTP_METHOD,
            ori.getProperty(Constants.Configuration.HTTP_METHOD));
        //coping the Message type from req to res to get the message formatters working correctly.
        newMC.setProperty(Constants.Configuration.MESSAGE_TYPE,
                ori.getProperty(Constants.Configuration.MESSAGE_TYPE));

        newMC.setDoingREST(ori.isDoingREST());
        newMC.setDoingMTOM(ori.isDoingMTOM());
        newMC.setDoingSwA(ori.isDoingSwA());

        // if the original request carries any attachments, copy them to the clone
        // as well, except for the soap part if any
        Attachments attachments = ori.getAttachmentMap();
        if (attachments != null && attachments.getAllContentIDs().length > 0) {
            String[] cIDs = attachments.getAllContentIDs();
            String soapPart = attachments.getSOAPPartContentID();
            for (String cID : cIDs) {
                if (!cID.equals(soapPart)) {
                    newMC.addAttachment(cID, attachments.getDataHandler(cID), attachments.getHeaders());
                }
            }
        }

        Iterator itr = ori.getPropertyNames();
        while (itr.hasNext()) {
            String key = (String) itr.next();
            if (key != null) {
                // In a clustered environment, all the properties that need to be replicated,
                // are replicated explicitly  by the corresponding Mediators (Ex: throttle,
                // cache), and therefore we should avoid any implicit replication
                newMC.setNonReplicableProperty(key, ori.getPropertyNonReplicable(key));
            }
        }

        if (isAggregate) {
            newMC.setServerSide(ori.isServerSide());
        } else {
            newMC.setServerSide(false);
        }

        return newMC;
    }

    /**
     * In an effort to refactor the code, this method is deprecated.
     * Use {@link #clonePartially(org.apache.axis2.context.MessageContext, boolean)} with the isAggregate flag true
     */
    @Deprecated
    public static org.apache.axis2.context.MessageContext clonePartiallyForAggregate(
            org.apache.axis2.context.MessageContext ori) throws AxisFault {
            return clonePartially(ori, true);
        }

    /**
     * This method will clone the provided SOAPEnvelope and returns the cloned envelope
     * as an exact copy of the provided envelope
     *
     * @param envelope - this will be cloned to get the new envelope
     * @return cloned SOAPEnvelope from the provided one
     */
    public static SOAPEnvelope cloneSOAPEnvelope(SOAPEnvelope envelope) {
        SOAPFactory fac;
        if (SOAP11Constants.SOAP_ENVELOPE_NAMESPACE_URI
                .equals(envelope.getBody().getNamespace().getNamespaceURI())) {
            fac = OMAbstractFactory.getSOAP11Factory();
        } else {
            fac = OMAbstractFactory.getSOAP12Factory();
        }
        SOAPEnvelope newEnvelope = fac.getDefaultEnvelope();
        Iterator childIterator;
        if (envelope.getHeader() != null) {
            SOAPHeader body = envelope.getHeader();
            childIterator = body.getChildren();
            while (childIterator.hasNext()) {
                Object bodyNs = childIterator.next();
                if (bodyNs instanceof SOAPHeaderBlock) {
                    try {
                        newEnvelope.getHeader()
                                .addChild(ElementHelper.toSOAPHeaderBlock(((OMElement) bodyNs).cloneOMElement(), fac));
                    } catch (Exception e) {
                        handleException(e);
                    }
                } else if (bodyNs instanceof OMElement) {
                    newEnvelope.getHeader().addChild(((OMElement) bodyNs).cloneOMElement());
                }
            }
        }

        if (envelope.getBody() != null) {
            // treat the SOAPFault cloning as a special case otherwise a cloning OMElement as the
            // fault would lead to class cast exceptions if accessed through the getFault method
        	if (envelope.getBody().getFirstElement() instanceof SOAPFault && envelope.getBody().hasFault()) {
                SOAPFault fault = envelope.getBody().getFault();
                newEnvelope.getBody().addFault(cloneSOAPFault(fault));
            } else {
                OMElement body = envelope.getBody().cloneOMElement();
                Iterator ns = body.getAllDeclaredNamespaces();
                OMNamespace bodyNs = body.getNamespace();
                String nsUri = bodyNs.getNamespaceURI();
                String nsPrefix = bodyNs.getPrefix();
                while (ns.hasNext()) {
                    OMNamespace namespace = ((OMNamespace)ns.next());
                    if (nsUri != null && !nsUri.equals(namespace.getNamespaceURI())
                        && nsPrefix != null && !nsPrefix.equals(namespace.getPrefix())) {
                        newEnvelope.getBody().declareNamespace(namespace);
                    }
                    ns.remove();
                }
                Iterator attributes = body.getAllAttributes();
                while (attributes.hasNext()) {
                    OMAttribute attrb = (OMAttribute) attributes.next();
                    newEnvelope.getBody().addAttribute(attrb);
                    attributes.remove();
                }
                Iterator itr = body.getChildren();
                while (itr.hasNext()) {
                    OMNode node = (OMNode) itr.next();
                    itr.remove();
                    newEnvelope.getBody().addChild(node);
                }
                /**
                 * Copy the namespaces declared in envelope , Fix for
                 * https://wso2.org/jira/browse/CARBON-16086
                 */
                Iterator allDeclaredNamespaces = envelope.getAllDeclaredNamespaces();
                while (allDeclaredNamespaces.hasNext()) {
                    newEnvelope.declareNamespace((OMNamespace) allDeclaredNamespaces.next());
                }
            }
        }

        return newEnvelope;
    }

    /**
     * Clones the given {@link org.apache.axis2.client.Options} object. This is not a deep copy
     * because this will be called for each and every message going out from synapse. The parent
     * of the cloning options object is kept as a reference.
     *
     * @param options cloning object
     * @return cloned Options object
     */
    public static Options cloneOptions(Options options) {

        // create new options object and set the parent
        Options clonedOptions = new Options(options.getParent());

        // copy general options
        clonedOptions.setCallTransportCleanup(options.isCallTransportCleanup());
        clonedOptions.setExceptionToBeThrownOnSOAPFault(options.isExceptionToBeThrownOnSOAPFault());
        clonedOptions.setManageSession(options.isManageSession());
        clonedOptions.setSoapVersionURI(options.getSoapVersionURI());
        clonedOptions.setTimeOutInMilliSeconds(options.getTimeOutInMilliSeconds());
        clonedOptions.setUseSeparateListener(options.isUseSeparateListener());

        // copy transport related options
        clonedOptions.setListener(options.getListener());
        clonedOptions.setTransportIn(options.getTransportIn());
        clonedOptions.setTransportInProtocol(options.getTransportInProtocol());
        clonedOptions.setTransportOut(options.getTransportOut());

        // copy username and password options
        clonedOptions.setUserName(options.getUserName());
        clonedOptions.setPassword(options.getPassword());

        // cloen the property set of the current options object
        for (Object o : options.getProperties().keySet()) {
            String key = (String) o;
            clonedOptions.setProperty(key, options.getProperty(key));
        }

        return clonedOptions;
    }

    /**
     * Removes Submission and Final WS-Addressing headers and return the SOAPEnvelope from the given
     * message context
     *
     * @param axisMsgCtx the Axis2 Message context
     * @return the resulting SOAPEnvelope
     */
    public static SOAPEnvelope removeAddressingHeaders(
            org.apache.axis2.context.MessageContext axisMsgCtx) {

        SOAPEnvelope env = axisMsgCtx.getEnvelope();
        SOAPHeader soapHeader = env.getHeader();
        ArrayList addressingHeaders;

        if (soapHeader != null) {
            addressingHeaders =
                soapHeader.getHeaderBlocksWithNSURI(AddressingConstants.Submission.WSA_NAMESPACE);

            if (addressingHeaders != null && addressingHeaders.size() != 0) {
                detachAddressingInformation(addressingHeaders);

            } else {
                addressingHeaders =
                    soapHeader.getHeaderBlocksWithNSURI(AddressingConstants.Final.WSA_NAMESPACE);
                if (addressingHeaders != null && addressingHeaders.size() != 0) {
                    detachAddressingInformation(addressingHeaders);
                }
            }
        }
        return env;
    }

    /**
     * Remove WS-A headers
     *
     * @param headerInformation headers to be removed
     */
    private static void detachAddressingInformation(ArrayList headerInformation) {
        for (Object o : headerInformation) {
            if (o instanceof SOAPHeaderBlock) {
                SOAPHeaderBlock headerBlock = (SOAPHeaderBlock) o;
                headerBlock.detach();
            } else if (o instanceof OMElement) {
                // work around for a known addressing bug which sends non SOAPHeaderBlock objects
                OMElement om = (OMElement) o;
                OMNamespace ns = om.getNamespace();
                if (ns != null && (
                    AddressingConstants.Submission.WSA_NAMESPACE.equals(ns.getNamespaceURI()) ||
                        AddressingConstants.Final.WSA_NAMESPACE.equals(ns.getNamespaceURI()))) {
                    om.detach();
                }
            }
        }
    }

    /**
     * Get the Policy object for the given name from the Synapse configuration at runtime
     *
     * @param synCtx the current synapse configuration to get to the synapse configuration
     * @param propertyKey the name of the property which holds the Policy required
     * @return the Policy object with the given name, from the configuration
     */
    public static Policy getPolicy(org.apache.synapse.MessageContext synCtx, String propertyKey) {
        Object property = synCtx.getEntry(propertyKey);
        if (property != null && property instanceof OMElement) {
            return PolicyEngine.getPolicy((OMElement) property);
        } else {
            handleException("Cannot locate policy from the property : " + propertyKey);
        }
        return null;
    }

    /**
     * Clones the SOAPFault, fault cloning is not the same as cloning the OMElement because if the
     * Fault is accessed through the SOAPEnvelope.getBody().getFault() method it will lead to a
     * class cast because the cloned element is just an OMElement but not a Fault.
     *
     * @param fault that needs to be cloned
     * @return the cloned fault
     */
    public static SOAPFault cloneSOAPFault(SOAPFault fault) {

        SOAPFactory fac;
        int soapVersion;
        final int SOAP_11 = 1;
        final int SOAP_12 = 2;
        if (SOAP11Constants.SOAP_ENVELOPE_NAMESPACE_URI
                .equals(fault.getNamespace().getNamespaceURI())) {
            fac = OMAbstractFactory.getSOAP11Factory();
            soapVersion = SOAP_11;
        } else {
            fac = OMAbstractFactory.getSOAP12Factory();
            soapVersion = SOAP_12;
        }
        SOAPFault newFault = fac.createSOAPFault();

        SOAPFaultCode code = fac.createSOAPFaultCode();
        SOAPFaultReason reason = fac.createSOAPFaultReason();

        switch (soapVersion) {
            case SOAP_11:
                code.setText(fault.getCode().getTextAsQName());
                reason.setText(fault.getReason().getText());
                break;
            case SOAP_12:
                SOAPFaultValue value = fac.createSOAPFaultValue(code);
                value.setText(fault.getCode().getTextAsQName());
                for (Object obj : fault.getReason().getAllSoapTexts()) {
                    SOAPFaultText text = fac.createSOAPFaultText();
                    text.setText(((SOAPFaultText) obj).getText());
                    reason.addSOAPText(text);
                }
                break;
        }

        newFault.setCode(code);
        newFault.setReason(reason);

        if (fault.getNode() != null) {
            SOAPFaultNode soapfaultNode = fac.createSOAPFaultNode();
            soapfaultNode.setNodeValue(fault.getNode().getNodeValue());
            newFault.setNode(soapfaultNode);
        }

        if (fault.getRole() != null) {
            SOAPFaultRole soapFaultRole = fac.createSOAPFaultRole();
            soapFaultRole.setRoleValue(fault.getRole().getRoleValue());
            newFault.setRole(soapFaultRole);
        }

        if (fault.getDetail() != null) {
            SOAPFaultDetail soapFaultDetail = fac.createSOAPFaultDetail();
            for (Iterator itr = fault.getDetail().getAllDetailEntries(); itr.hasNext();) {
            	Object element = itr.next();
				if (element instanceof OMElement) {
					soapFaultDetail.addDetailEntry(((OMElement) element).cloneOMElement());
				}
            }
            newFault.setDetail(soapFaultDetail);
        }

        return newFault;
    }

    /**
     * Remove the headers that are marked as processed.
     * @param axisMsgCtx the Axis2 Message context
     * @param preserveAddressing if true preserve the addressing headers
     */
    public static void removeProcessedHeaders(org.apache.axis2.context.MessageContext axisMsgCtx,
                                              boolean preserveAddressing) {
        SOAPEnvelope env = axisMsgCtx.getEnvelope();
        SOAPHeader soapHeader = env.getHeader();

        if (soapHeader != null) {
            Iterator it = soapHeader.getChildElements();
            while (it.hasNext()) {
                Object o = it.next();
                if (o instanceof SOAPHeaderBlock) {
                    SOAPHeaderBlock headerBlock = (SOAPHeaderBlock) o;
                    if (!preserveAddressing) {
                        // if we don't need to preserve addressing headers remove without checking
                        if (headerBlock.isProcessed()) {
                            headerBlock.detach();
                        }
                    } else {
                        // else remove only if not an addressing header
                        if (!isAddressingHeader(headerBlock)) {
                            if (headerBlock.isProcessed()) {
                                headerBlock.detach();
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Copy the response message headers received from the back-end response to the axis2 message context.
     *
     * @param resultAxisMsgCtx axis2 message context of the response received from the backend
     * @param axisInMsgCtx axis2 message context of the response to be sent to the client
     */
    public static void copyResponseMessageHeaders(org.apache.axis2.context.MessageContext resultAxisMsgCtx,
                                                  org.apache.axis2.context.MessageContext axisInMsgCtx) {
        Object transportHeadersObj = resultAxisMsgCtx.getProperty(
                org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS);
        axisInMsgCtx.setProperty(
                org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS, transportHeadersObj);
        if (transportHeadersObj instanceof Map) {
            Map transportHeaders = (Map) transportHeadersObj;
            String headerContentType = (String) transportHeaders.get(HTTPConstants.HEADER_CONTENT_TYPE);
            if (headerContentType != null) {
                // In case the Content-Type header comes with a different case (ex: Content-type), we are removing it
                // from the transport headers and putting it again with the key = "Content-Type" to avoid duplicate
                //  headers in the response.
                transportHeaders.remove(HTTPConstants.HEADER_CONTENT_TYPE);
                transportHeaders.put(HTTPConstants.HEADER_CONTENT_TYPE, headerContentType);
                try {
                    String mimeType = new ContentType(headerContentType).getBaseType();
                    axisInMsgCtx.setProperty(Constants.Configuration.CONTENT_TYPE, mimeType);
                    axisInMsgCtx.setProperty(Constants.Configuration.MESSAGE_TYPE, mimeType);
                } catch (ParseException e) {
                    String defaultContentType = (String) axisInMsgCtx.getProperty(
                            Constants.Configuration.CONTENT_TYPE);
                    String defaultMessageType = (String) axisInMsgCtx.getProperty(
                            Constants.Configuration.MESSAGE_TYPE);
                    log.error("Error occurred while parsing ContentType header: " + headerContentType
                            + ". in the response. Therefore, using the default ContentType: "
                            + defaultContentType + " and MessageType: " + defaultMessageType, e);
                }
            }
        }
    }

    /**
     * Return true if the SOAP header is an addressing header
     * @param headerBlock SOAP header block to be checked
     * @return true if the SOAP header is an addressing header
     */
    private static boolean isAddressingHeader(SOAPHeaderBlock headerBlock) {
        OMNamespace ns = headerBlock.getNamespace();
        return ns != null && (
                AddressingConstants.Submission.WSA_NAMESPACE.equals(ns.getNamespaceURI()) ||
                        AddressingConstants.Final.WSA_NAMESPACE.equals(ns.getNamespaceURI()));
    }

    private static void handleException(String msg) {
        log.error(msg);
        throw new SynapseException(msg);
    }
    private static void handleException(Exception e) {
        log.error(e);
        throw new SynapseException(e);
    }

    /**
     * This method is to set mediatorId property to axis2 message context. This Id will be copied to iosession from the DeliveryAgent.java
     * class and it will be used at wire level to identify to which mediator the wirelogs belongs.
     *
     * @param synCtx
     */
    public static void setWireLogHolderProperties(MessageContext synCtx, boolean isBreakPoint, SynapseMediationFlowPoint mediationFlowPoint) {
        if (isBreakPoint) {
            String mediatorId = synCtx.getEnvironment().getSynapseDebugManager().createDebugMediationFlowPointJSONForWireLogs(mediationFlowPoint).toString();
            ((Axis2MessageContext) synCtx).getAxis2MessageContext().setProperty(SynapseDebugInfoHolder.SYNAPSE_WIRE_LOG_MEDIATOR_ID_PROPERTY, mediatorId);
        } else {
            /**
             * this is to be used when there are mediators which sends back-end requests, but not a break point.
             */
            ((Axis2MessageContext) synCtx).getAxis2MessageContext().setProperty(SynapseDebugInfoHolder.SYNAPSE_WIRE_LOG_MEDIATOR_ID_PROPERTY,
                                                                                SynapseDebugInfoHolder.DUMMY_MEDIATOR_ID); //
        }
    }

    /**
     * Mask the password of the connection url with ***
     * @param url the actual url
     * @return the masked url
     */
    public static String maskURLPassword(String url) {
        final Matcher urlMatcher = URL_PATTERN.matcher(url);
        String maskUrl;
        if (urlMatcher.find()) {
            final Matcher pwdMatcher = PASSWORD_PATTERN.matcher(url);
            maskUrl = pwdMatcher.replaceFirst(":***@");
            return maskUrl;
        }
        return url;
    }

}

