/*
 *  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.transport.base;

import org.apache.axis2.engine.AxisConfiguration;
import org.apache.axis2.description.AxisService;
import org.apache.axis2.description.Parameter;
import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
import org.apache.axis2.builder.BuilderUtil;
import org.apache.axis2.transport.MessageFormatter;
import org.apache.axis2.transport.TransportUtils;
import org.apache.axis2.transport.http.HTTPTransportUtils;
import org.apache.synapse.format.BinaryFormatter;
import org.apache.synapse.format.PlainTextFormatter;
import org.apache.synapse.transport.vfs.PollTableEntry;
import org.apache.axis2.context.MessageContext;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.axiom.soap.*;
import org.apache.axiom.soap.impl.builder.StAXSOAPModelBuilder;
import org.apache.axiom.soap.impl.llom.soap11.SOAP11Factory;
import org.apache.axiom.om.util.StAXUtils;
import org.apache.axiom.om.impl.builder.StAXBuilder;
import org.apache.axiom.om.impl.builder.StAXOMBuilder;
import org.apache.axiom.om.impl.llom.OMTextImpl;
import org.apache.axiom.om.OMOutputFormat;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMText;
import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.attachments.ByteArrayDataSource;

import javax.mail.internet.ContentType;
import javax.mail.internet.ParseException;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.activation.DataHandler;
import java.io.InputStream;
import java.io.IOException;
import java.util.List;
import java.util.Iterator;

public abstract class BaseUtils {

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

    /**
     * Return a QName from the String passed in of the form {ns}element
     * @param obj a QName or a String containing a QName in {ns}element form
     * @return a corresponding QName object
     */
    public static QName getQNameFromString(Object obj) {
        String value;
        if (obj instanceof QName) {
            return (QName) obj;
        } else {
            value = obj.toString();
        }
        int open = value.indexOf('{');
        int close = value.indexOf('}');
        if (close > open && open > -1 && value.length() > close) {
            return new QName(value.substring(open+1, close-open), value.substring(close+1));
        } else {
            return new QName(value);
        }
    }

    /**
     * Marks the given service as faulty with the given comment
     *
     * @param serviceName service name
     * @param msg         comment for being faulty
     * @param axisCfg     configuration context
     */
    public static void markServiceAsFaulty(String serviceName, String msg,
                                           AxisConfiguration axisCfg) {
        if (serviceName != null) {
            try {
                AxisService service = axisCfg.getService(serviceName);
                axisCfg.getFaultyServices().put(service.getName(), msg);

            } catch (AxisFault axisFault) {
                log.warn("Error marking service : " + serviceName + " as faulty", axisFault);
            }
        }
    }

    /**
     * Create a SOAP envelope using SOAP 1.1 or 1.2 depending on the namespace
     * @param in InputStream for the payload
     * @param namespace the SOAP namespace
     * @return the SOAP envelope for the correct version
     * @throws javax.xml.stream.XMLStreamException on error
     */
    public static SOAPEnvelope getEnvelope(InputStream in, String namespace) throws XMLStreamException {

        try {
            in.reset();
        } catch (IOException ignore) {}
        XMLStreamReader xmlreader =
            StAXUtils.createXMLStreamReader(in, MessageContext.DEFAULT_CHAR_SET_ENCODING);
        StAXBuilder builder = new StAXSOAPModelBuilder(xmlreader, namespace);
        return (SOAPEnvelope) builder.getDocumentElement();
    }

    /**
     * Get the OMOutput format for the given message
     * @param msgContext the axis message context
     * @return the OMOutput format to be used
     */
    public static OMOutputFormat getOMOutputFormat(MessageContext msgContext) {

        OMOutputFormat format = new OMOutputFormat();
        msgContext.setDoingMTOM(HTTPTransportUtils.doWriteMTOM(msgContext));
        msgContext.setDoingSwA(HTTPTransportUtils.doWriteSwA(msgContext));
        msgContext.setDoingREST(HTTPTransportUtils.isDoingREST(msgContext));
        format.setSOAP11(msgContext.isSOAP11());
        format.setDoOptimize(msgContext.isDoingMTOM());
        format.setDoingSWA(msgContext.isDoingSwA());

        format.setCharSetEncoding(HTTPTransportUtils.getCharSetEncoding(msgContext));
        Object mimeBoundaryProperty = msgContext.getProperty(Constants.Configuration.MIME_BOUNDARY);
        if (mimeBoundaryProperty != null) {
            format.setMimeBoundary((String) mimeBoundaryProperty);
        }
        return format;
    }
    
    /**
     * Get the MessageFormatter for the given message.
     * @param msgContext the axis message context
     * @return the MessageFormatter to be used
     */
    public static MessageFormatter getMessageFormatter(MessageContext msgContext) {
        // check the first element of the SOAP body, do we have content wrapped using the
        // default wrapper elements for binary (BaseConstants.DEFAULT_BINARY_WRAPPER) or
        // text (BaseConstants.DEFAULT_TEXT_WRAPPER) ? If so, select the appropriate
        // message formatter directly ...
        OMElement firstChild = msgContext.getEnvelope().getBody().getFirstElement();
        if (firstChild != null) {
            if (BaseConstants.DEFAULT_BINARY_WRAPPER.equals(firstChild.getQName())) {
                return new BinaryFormatter();
            } else if (BaseConstants.DEFAULT_TEXT_WRAPPER.equals(firstChild.getQName())) {
                return new PlainTextFormatter();
            }
        }
        
        // ... otherwise, let Axis choose the right message formatter:
        try {
            return TransportUtils.getMessageFormatter(msgContext);
        } catch (AxisFault axisFault) {
            throw new BaseTransportException("Unable to get the message formatter to use");
        }
    }

    public static long getMinPollTime(List pollTable) {
        Iterator iter = pollTable.iterator();
        long min = AbstractPollingTransportListener.DEFAULT_POLL_INTERVAL;

        while (iter.hasNext()) {
            PollTableEntry entry = (PollTableEntry) iter.next();
            if (entry.getPollInterval() < min) {
                min = entry.getPollInterval();
            }
        }
        return min;
    }

    /**
     * Create a SOAPEnvelope from the given message and set it into
     * the axis MessageContext passed
     *
     * @param message the message object
     * @param msgContext the axis MessageContext
     * @param contentType
     * @throws AxisFault on errors encountered while setting the envelope to the message context
     */
    public void setSOAPEnvelope(Object message, MessageContext msgContext, String contentType)
            throws AxisFault {

        SOAPEnvelope envelope = null;
        StAXBuilder builder = null;

        String charSetEnc = null;
        try {
            if (contentType != null) {
                charSetEnc = new ContentType(contentType).getParameter("charset");
            }
        } catch (ParseException ex) {
            // ignore
        }
        
        InputStream in = getInputStream(message);

        // handle SOAP payloads when a correct content type is available
        try {
            if (contentType != null) {
                if (contentType.indexOf(BaseConstants.MULTIPART_RELATED) > -1) {
                    builder = BuilderUtil.getAttachmentsBuilder(msgContext, in, contentType, true);
                    envelope = (SOAPEnvelope) builder.getDocumentElement();

                } else {
                    builder = BuilderUtil.getSOAPBuilder(in, charSetEnc);
                    envelope = (SOAPEnvelope) builder.getDocumentElement();
                }
            }
        } catch (Exception ignore) {
            try {
                in.close();
            } catch (IOException e) {
                // ignore
            }
            in = getInputStream(message);
        }


        // handle SOAP when content type is missing, or any other POX, binary or text payload
        if (builder == null) {

            SOAPFactory soapFactory = new SOAP11Factory();
            try {
                builder = new StAXOMBuilder(StAXUtils.createXMLStreamReader(in, charSetEnc));
                builder.setOMBuilderFactory(OMAbstractFactory.getOMFactory());
                String ns = builder.getDocumentElement().getNamespace().getNamespaceURI();

                if (SOAP12Constants.SOAP_ENVELOPE_NAMESPACE_URI.equals(ns)) {
                    envelope = BaseUtils.getEnvelope(in, SOAP12Constants.SOAP_ENVELOPE_NAMESPACE_URI);

                } else if (SOAP11Constants.SOAP_ENVELOPE_NAMESPACE_URI.equals(ns)) {
                    envelope = BaseUtils.getEnvelope(in, SOAP11Constants.SOAP_ENVELOPE_NAMESPACE_URI);

                } else {
                    // this is POX ... mark message as REST
                    msgContext.setDoingREST(true);
                    envelope = soapFactory.getDefaultEnvelope();
                    envelope.getBody().addChild(builder.getDocumentElement());
                }

            } catch (Exception e) {
                envelope = handleLegacyMessage(msgContext, message);
            }
        }

        // Set the encoding scheme in the message context
        msgContext.setProperty(Constants.Configuration.CHARACTER_SET_ENCODING, charSetEnc);

        String charEncOfMessage =
            builder == null ? null :
                builder.getDocument() == null ? null : builder.getDocument().getCharsetEncoding();

        if (StringUtils.isNotBlank(charEncOfMessage) &&
            StringUtils.isNotBlank(charSetEnc) &&
            !charEncOfMessage.equalsIgnoreCase(charSetEnc)) {
            handleException("Charset encoding of transport differs from that of the payload");
        }

        msgContext.setEnvelope(envelope);
    }

    /**
     * Handle a non SOAP and non XML message, and create a SOAPEnvelope to hold the
     * pure text or binary content as necessary
     *
     * @param msgContext the axis message context
     * @param message the legacy message
     * @return the SOAP envelope
     */
    private SOAPEnvelope handleLegacyMessage(MessageContext msgContext, Object message) {

        SOAPFactory soapFactory = new SOAP11Factory();
        SOAPEnvelope envelope;

        if (log.isDebugEnabled()) {
            log.debug("Non SOAP/XML message received");
        }

        // pick the name of the element that will act as the wrapper element for the
        // non-xml payload. If service doesn't define one, default
        Parameter wrapperParam = msgContext.getAxisService().
            getParameter(BaseConstants.WRAPPER_PARAM);

        QName wrapperQName = null;
        OMElement wrapper = null;
        if (wrapperParam != null) {
            wrapperQName = BaseUtils.getQNameFromString(wrapperParam.getValue());
        }

        String textPayload = getMessageTextPayload(message);
        if (textPayload != null) {
            OMTextImpl textData = (OMTextImpl) soapFactory.createOMText(textPayload);

            if (wrapperQName == null) {
                wrapperQName = BaseConstants.DEFAULT_TEXT_WRAPPER;
            }
            wrapper = soapFactory.createOMElement(wrapperQName, null);
            wrapper.addChild(textData);

        } else {
            byte[] msgBytes = getMessageBinaryPayload(message);
            if (msgBytes != null) {
                DataHandler dataHandler = new DataHandler(new ByteArrayDataSource(msgBytes));
                OMText textData = soapFactory.createOMText(dataHandler, true);
                if (wrapperQName == null) {
                    wrapperQName = BaseConstants.DEFAULT_BINARY_WRAPPER;
                }
                wrapper = soapFactory.createOMElement(wrapperQName, null);
                wrapper.addChild(textData);
                msgContext.setDoingMTOM(true);
                
            } else {
                handleException("Unable to read payload from message of type : "
                    + message.getClass().getName());
            }
        }

        envelope = soapFactory.getDefaultEnvelope();
        envelope.getBody().addChild(wrapper);

        return envelope;
    }

     /**
     * Get a String property from a message
     *
     * @param message the message object
     * @param property property name
     * @return property value
     */
    public abstract String getProperty(Object message, String property);

    /**
     * Get an InputStream to the message payload
     *
     * @param message Object
     * @return an InputStream to the payload
     */
    public abstract InputStream getInputStream(Object message);

    /**
     * Get the message payload as a String, if the message is a non-SOAP, non-XML, plain text message
     *
     * @param message the message Object
     * @return the plain text payload of the message if applicable
     */
    public abstract String getMessageTextPayload(Object message);

    /**
     * Get the message payload as a byte[], if the message is a non-SOAP, non-XML, binary message
     *
     * @param message the message Object
     * @return the payload of the message as a byte[]
     */
    public abstract byte[] getMessageBinaryPayload(Object message);

    protected static void handleException(String s) {
        log.error(s);
        throw new BaseTransportException(s);
    }

    protected static void handleException(String s, Exception e) {
        log.error(s, e);
        throw new BaseTransportException(s, e);
    }

    /**
     * Utility method to check if a string is not null and not empty
     * @param str the string to check
     * @return true if not null and not empty
     */
    public static boolean isValid(String str) {
        return (str != null && str.trim().length() > 0);
    }

    public static String getRequiredServiceParam(AxisService service, String paramName) throws AxisFault {
        Parameter param = service.getParameter(paramName);
        if (param != null && param.getValue() != null && param.getValue() instanceof String) {
            return (String) param.getValue();
        } else {
            throw new AxisFault("Cannot find parameter : " + paramName +
                " for service : " + service.getName());
        }
    }

    public static String getOptionalServiceParam(AxisService service, String paramName) throws AxisFault {
        Parameter param = service.getParameter(paramName);
        if (param != null && param.getValue() != null && param.getValue() instanceof String) {
            return (String) param.getValue();
        } else {
            return null;
        }
    }

    public static boolean isUsingTransport(AxisService service, String transportName) {
        boolean process = service.isEnableAllTransports();
        if (process) {
            return true;

        } else {
            List transports = service.getExposedTransports();
            for (Object transport : transports) {
                if (transportName.equals(transport)) {
                    return true;
                }
            }
        }
        return false;
    }
}
