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

import org.apache.axiom.om.OMOutputFormat;
import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
import org.apache.axis2.addressing.EndpointReference;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.context.NamedValue;
import org.apache.axis2.description.Parameter;
import org.apache.axis2.description.TransportOutDescription;
import org.apache.axis2.handlers.AbstractHandler;
import org.apache.axis2.kernel.http.HTTPConstants;
import org.apache.axis2.kernel.MessageFormatter;
import org.apache.axis2.kernel.OutTransportInfo;
import org.apache.axis2.kernel.TransportUtils;
import org.apache.axis2.transport.http.server.AxisHttpResponse;
import org.apache.axis2.util.JavaUtils;
import org.apache.axis2.util.MessageProcessorSelector;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.xml.stream.FactoryConfigurationError;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPOutputStream;

/**
 * Base class for Apache HttpClient (3.1 or 4.x) based transport senders.
 */
public abstract class AbstractHTTPTransportSender extends AbstractHandler implements HTTPTransportSender {
    /**
     * The {@link TransportOutDescription} object received by the call to
     * {@link #init(ConfigurationContext, TransportOutDescription)}.
     */
    private TransportOutDescription transportOut;

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

    /**
     * Default HTTP version as configured in <tt>axis2.xml</tt>. This may be overridden on a per
     * message basis using the {@link HTTPConstants#HTTP_PROTOCOL_VERSION} property.
     */
    private String defaultHttpVersion = HTTPConstants.HEADER_PROTOCOL_11;

    /**
     * Specifies whether chunked encoding is enabled by default. This is configured in
     * <tt>axis2.xml</tt> and may be overridden on a per message basis using the
     * {@link HTTPConstants#CHUNKED} property.
     */
    private boolean defaultChunked = false;

    private int soTimeout = HTTPConstants.DEFAULT_SO_TIMEOUT;

    private int connectionTimeout = HTTPConstants.DEFAULT_CONNECTION_TIMEOUT;
    
    public void init(ConfigurationContext confContext,
                     TransportOutDescription transportOut) throws AxisFault {
        this.transportOut = transportOut;
        //Set HTTP client version
        setHTTPClientVersion(confContext);
        
        // <parameter name="PROTOCOL">HTTP/1.0</parameter> or
        // <parameter name="PROTOCOL">HTTP/1.1</parameter> or
        // <parameter name="PROTOCOL">HTTP/2.0</parameter> is
        // checked
        Parameter version = transportOut
                .getParameter(HTTPConstants.PROTOCOL_VERSION);
        if (version != null) {
            if (HTTPConstants.HEADER_PROTOCOL_11.equals(version.getValue())) {
                defaultHttpVersion = HTTPConstants.HEADER_PROTOCOL_11;

                Parameter transferEncoding = transportOut
                        .getParameter(HTTPConstants.HEADER_TRANSFER_ENCODING);

                if ((transferEncoding != null)
                        && HTTPConstants.HEADER_TRANSFER_ENCODING_CHUNKED
                        .equals(transferEncoding.getValue())) {
                    defaultChunked = true;
                }
            } else if (HTTPConstants.HEADER_PROTOCOL_10.equals(version
                    .getValue())) {
                defaultHttpVersion = HTTPConstants.HEADER_PROTOCOL_10;
            } else if (HTTPConstants.HEADER_PROTOCOL_20.equals(version
                    .getValue())) {
                defaultHttpVersion = HTTPConstants.HEADER_PROTOCOL_20;
                // HTTP/2.0 supports multiplexing, so enable chunked by default
                defaultChunked = true;
            } else {
                throw new AxisFault("Parameter "
                        + HTTPConstants.PROTOCOL_VERSION
                        + " Can have values only HTTP/1.0, HTTP/1.1, or HTTP/2.0");
            }
        }

        // Get the timeout values from the configuration
        try {
            Parameter tempSoTimeoutParam = transportOut
                    .getParameter(HTTPConstants.SO_TIMEOUT);
            Parameter tempConnTimeoutParam = transportOut
                    .getParameter(HTTPConstants.CONNECTION_TIMEOUT);

            if (tempSoTimeoutParam != null) {
                soTimeout = Integer.parseInt((String) tempSoTimeoutParam
                        .getValue());
            }

            if (tempConnTimeoutParam != null) {
                connectionTimeout = Integer
                        .parseInt((String) tempConnTimeoutParam.getValue());
            }
        } catch (NumberFormatException nfe) {

            // If there's a problem log it and use the default values
            log.error("Invalid timeout value format: not a number", nfe);
        }
    }

    public void stop() {
        // Any code that , need to invoke when sender stop
    }

    public InvocationResponse invoke(MessageContext msgContext)
            throws AxisFault {
        try {
            OMOutputFormat format = new OMOutputFormat();
            // if (!msgContext.isDoingMTOM())
            msgContext.setDoingMTOM(TransportUtils.doWriteMTOM(msgContext));
            msgContext.setDoingSwA(TransportUtils.doWriteSwA(msgContext));
            msgContext.setDoingREST(TransportUtils.isDoingREST(msgContext));
            format.setSOAP11(msgContext.isSOAP11());
            format.setDoOptimize(msgContext.isDoingMTOM());
            format.setDoingSWA(msgContext.isDoingSwA());
            format.setCharSetEncoding(TransportUtils.getCharSetEncoding(msgContext));

            Object mimeBoundaryProperty = msgContext
                    .getProperty(Constants.Configuration.MIME_BOUNDARY);
            if (mimeBoundaryProperty != null) {
                format.setMimeBoundary((String) mimeBoundaryProperty);
            }

            // set the property values only if they are not set by the user explicitly
            if (msgContext.getProperty(HTTPConstants.SO_TIMEOUT) == null) {
                msgContext.setProperty(HTTPConstants.SO_TIMEOUT, soTimeout);
            }

            if (msgContext.getProperty(HTTPConstants.CONNECTION_TIMEOUT) == null) {
                msgContext.setProperty(HTTPConstants.CONNECTION_TIMEOUT, connectionTimeout);
            }

            //if a parameter has set been set, we will omit the SOAP action for SOAP 1.2
            if (!msgContext.isSOAP11()) {
                Parameter param = transportOut.getParameter(HTTPConstants.OMIT_SOAP_12_ACTION);
                Object parameterValue = null;
                if (param != null) {
                    parameterValue = param.getValue();
                }

                if (parameterValue != null && JavaUtils.isTrueExplicitly(parameterValue)) {
                    //Check whether user has already overridden this.
                    Object propertyValue = msgContext.getProperty(
                            Constants.Configuration.DISABLE_SOAP_ACTION);

                    if (propertyValue == null || !JavaUtils.isFalseExplicitly(propertyValue)) {
                        msgContext.setProperty(Constants.Configuration.DISABLE_SOAP_ACTION,
                                Boolean.TRUE);
                    }
                }
            }

            // Transport URL can be different from the WSA-To. So processing
            // that now.
            EndpointReference epr = null;
            String transportURL = (String) msgContext
                    .getProperty(Constants.Configuration.TRANSPORT_URL);

            if (transportURL != null) {
                epr = new EndpointReference(transportURL);
            } else if (msgContext.getTo() != null
                    && !msgContext.getTo().hasAnonymousAddress()) {
                epr = msgContext.getTo();
            }

            // Check for the REST behavior, if you desire rest behavior
            // put a <parameter name="doREST" value="true"/> at the
            // server.xml/client.xml file
            // ######################################################
            // Change this place to change the wsa:toepr
            // epr = something
            // ######################################################

            if (epr != null) {
                if (!epr.hasNoneAddress()) {
                    writeMessageWithCommons(msgContext, epr, format);
                }else{
                	if(msgContext.isFault()){
                		if(log.isDebugEnabled()){
                			log.debug("Fault sent to WS-A None URI: "+msgContext.getEnvelope().getBody().getFault());
                		}
                	}
                }
            } else {
                if (msgContext.getProperty(MessageContext.TRANSPORT_OUT) != null) {
                    sendUsingOutputStream(msgContext, format);
                    TransportUtils.setResponseWritten(msgContext, true);
                } else {
                    throw new AxisFault("Both the TO and MessageContext.TRANSPORT_OUT property " +
                            "are null, so nowhere to send");
                }
            }
        } catch (FactoryConfigurationError e) {
            log.debug(e);
            throw AxisFault.makeFault(e);
        } catch (IOException e) {
            log.debug(e);
            throw AxisFault.makeFault(e);
        }
        return InvocationResponse.CONTINUE;
    }

    /**
     * Send a message (which must be a response) via the OutputStream sitting in the
     * MessageContext TRANSPORT_OUT property.  Since this class is used for both requests and
     * responses, we split the logic - this method always gets called when we're
     * writing to the HTTP response stream, and sendUsingCommons() is used for requests.
     *
     * @param msgContext the active MessageContext
     * @param format output formatter for our message
     * @throws AxisFault if a general problem arises
     */
    private void sendUsingOutputStream(MessageContext msgContext,
                                       OMOutputFormat format) throws AxisFault {
        OutputStream out = (OutputStream) msgContext.getProperty(MessageContext.TRANSPORT_OUT);

        // I Don't think we need this check.. Content type needs to be set in
        // any case. (thilina)
        // if (msgContext.isServerSide()) {
        OutTransportInfo transportInfo = (OutTransportInfo) msgContext
                .getProperty(Constants.OUT_TRANSPORT_INFO);

        if (transportInfo == null) throw new AxisFault("No transport info in MessageContext");

        ServletBasedOutTransportInfo servletBasedOutTransportInfo = null;
        if (transportInfo instanceof ServletBasedOutTransportInfo) {
            servletBasedOutTransportInfo =
                    (ServletBasedOutTransportInfo) transportInfo;

            // if sending a fault, set HTTP status code (respecting user-set status codes)
            if (msgContext.isFault()) {
                int faultStatus = resolveHttpStatusCode(msgContext,
                        HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                servletBasedOutTransportInfo.setStatus(faultStatus);
            }

            Object customHeaders = msgContext.getProperty(HTTPConstants.HTTP_HEADERS);
            if (customHeaders != null) {
                if (customHeaders instanceof List) {
                    Iterator iter = ((List) customHeaders).iterator();
                    while (iter.hasNext()) {
                        NamedValue nv = (NamedValue) iter.next();
                        if (nv != null) {
                            servletBasedOutTransportInfo
                                    .addHeader(nv.getName(), nv.getValue());
                        }
                    }
                } else if (customHeaders instanceof Map) {
                    Iterator iter = ((Map) customHeaders).entrySet().iterator();
                    while (iter.hasNext()) {
                        Map.Entry header = (Map.Entry) iter.next();
                        if (header != null) {
                            servletBasedOutTransportInfo
                                    .addHeader((String) header.getKey(), (String) header.getValue());
                        }
                    }
                }
            }
        } else if (transportInfo instanceof AxisHttpResponse) {
            if (msgContext.isFault()) {
                int faultStatus = resolveHttpStatusCode(msgContext,
                        HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                ((AxisHttpResponse) transportInfo).setStatus(faultStatus);
            }

            Object customHeaders = msgContext.getProperty(HTTPConstants.HTTP_HEADERS);
            if (customHeaders != null) {
                if (customHeaders instanceof List) {
                    Iterator iter = ((List) customHeaders).iterator();
                    while (iter.hasNext()) {
                        NamedValue nv = (NamedValue) iter.next();
                        if (nv != null) {
                            ((AxisHttpResponse) transportInfo)
                                    .addHeader(nv.getName(), nv.getValue());	
                        }
                    }
                } else if (customHeaders instanceof Map) {
                    Iterator iter = ((Map) customHeaders).entrySet().iterator();
                    while (iter.hasNext()) {
                        Map.Entry header = (Map.Entry) iter.next();
                        if (header != null) {
                            ((AxisHttpResponse) transportInfo)
                                    .addHeader((String) header.getKey(), (String) header.getValue());
                        }
                    }
                } else {
                    log.error("AbstractHTTPTransportSender.sendUsingOutputStream() received customHeaders of unrecognized type: " + customHeaders.getClass());
		}	
            }
        } else {
	    log.error("AbstractHTTPTransportSender.sendUsingOutputStream() found unknown type, no action taken  ... type: " + transportInfo);    
	}	

        MessageFormatter messageFormatter = MessageProcessorSelector.getMessageFormatter(msgContext);
        if (messageFormatter == null) throw new AxisFault("No MessageFormatter in MessageContext");

        // Once we get to this point, exceptions should NOT be turned into faults and sent,
        // because we're already sending!  So catch everything and log it, but don't pass
        // upwards.

        try {
            transportInfo.setContentType(
                messageFormatter.getContentType(msgContext, format, findSOAPAction(msgContext)));

            Object gzip = msgContext.getOptions().getProperty(HTTPConstants.MC_GZIP_RESPONSE);
            if (gzip != null && JavaUtils.isTrueExplicitly(gzip)) {
                if (servletBasedOutTransportInfo != null)
                    servletBasedOutTransportInfo.addHeader(HTTPConstants.HEADER_CONTENT_ENCODING,
                                                           HTTPConstants.COMPRESSION_GZIP);
                try {
                    out = new GZIPOutputStream(out);
                    messageFormatter.writeTo(msgContext, format, out, false);
                    ((GZIPOutputStream) out).finish();
                    out.flush();
                } catch (IOException e) {
                    throw new AxisFault("Could not compress response");
                }
            } else {
                messageFormatter.writeTo(msgContext, format, out, false);
            }
        } catch (AxisFault axisFault) {
            log.error(axisFault.getMessage(), axisFault);
            throw axisFault;
        }
    }

    /**
     * Resolves the HTTP status code to use for a response. Checks the message context
     * (and the inbound message context as a fallback) for a user-specified status code
     * via {@link Constants#HTTP_RESPONSE_STATE}. If none is found, returns the default.
     *
     * @param msgContext the current (outbound) message context
     * @param defaultStatus the default HTTP status code to use if no user override is found
     * @return the resolved HTTP status code
     */
    private static int resolveHttpStatusCode(MessageContext msgContext, int defaultStatus) {
        // Check the outbound message context first
        String statusStr = (String) msgContext.getProperty(Constants.HTTP_RESPONSE_STATE);

        // Fall back to the inbound message context (createFaultMessageContext may not
        // have copied this property in older code paths)
        if (statusStr == null) {
            MessageContext inMsgCtx =
                    (MessageContext) msgContext.getProperty(MessageContext.IN_MESSAGE_CONTEXT);
            if (inMsgCtx != null) {
                statusStr = (String) inMsgCtx.getProperty(Constants.HTTP_RESPONSE_STATE);
            }
        }

        if (statusStr != null) {
            try {
                return Integer.parseInt(statusStr);
            } catch (NumberFormatException e) {
                log.error("Invalid HTTP status code value: " + statusStr, e);
            }
        }
        return defaultStatus;
    }

    private void writeMessageWithCommons(MessageContext messageContext,
                                         EndpointReference toEPR, OMOutputFormat format)
            throws AxisFault {
        try {
            URL url = new URL(toEPR.getAddress());

            // select the Message Sender depending on the REST status
            HTTPSender sender = createHTTPSender();

            String httpVersion;
            if (messageContext.getProperty(HTTPConstants.HTTP_PROTOCOL_VERSION) != null) {
                httpVersion = (String) messageContext
                        .getProperty(HTTPConstants.HTTP_PROTOCOL_VERSION);
            } else {
                httpVersion = defaultHttpVersion;
            }
            
            boolean chunked;
            if (httpVersion.equals(HTTPConstants.HEADER_PROTOCOL_10)) {
                chunked = false;
            } else if (messageContext.getProperty(HTTPConstants.CHUNKED) != null) {
                chunked = JavaUtils.isTrueExplicitly(messageContext
                        .getProperty(HTTPConstants.CHUNKED));
            } else {
                chunked = defaultChunked;
            }

            // Following order needed to be preserved because,
            // HTTP/1.0 does not support chunk encoding
            sender.setChunked(chunked);
            sender.setHttpVersion(httpVersion);
            sender.setFormat(format);

            sender.send(messageContext, url, findSOAPAction(messageContext));
        } catch (MalformedURLException e) {
            log.debug(e);
            throw AxisFault.makeFault(e);
        }  catch (IOException e) {
            log.debug(e);
            throw AxisFault.makeFault(e);
        }
    }

    protected abstract HTTPSender createHTTPSender();

    /**
     * @param actionString
     * @return true if the specified String represents a generated (anonymous name)
     */
    public static boolean isGeneratedName(String actionString) {
        if (actionString == null) {
            return false;
        }
        
        // Different releases may have different constructed names or 
        // namespaces or quames.  However all equal or end with the following
        // sequences
        if (actionString.indexOf("anon") >= 0) {
            if (actionString.equals("anonOutInOp") ||
                actionString.endsWith(":anonOutInOp") ||
                actionString.endsWith("/anonOutInOp") ||
                actionString.endsWith("}anonOutInOp") ||

                actionString.equals("anonOutonlyOp") ||
                actionString.endsWith(":anonOutonlyOp") ||
                actionString.endsWith("/anonOutonlyOp") ||
                actionString.endsWith("}anonOutonlyOp") ||

                actionString.equals("anonRobustOp") ||
                actionString.endsWith(":anonRobustOp") ||
                actionString.endsWith("/anonRobustOp") ||
                actionString.endsWith("}anonRobustOp") ) {
                return true;
            }
        }
        return false;
    }

    
    private static String findSOAPAction(MessageContext messageContext) {
        String soapActionString = null;

        Parameter parameter =
                messageContext.getTransportOut().getParameter(HTTPConstants.OMIT_SOAP_12_ACTION);
        if (parameter != null && JavaUtils.isTrueExplicitly(parameter.getValue()) &&
            !messageContext.isSOAP11()) {
            return "\"\"";
        }

        Object disableSoapAction = messageContext.getOptions().getProperty(
                Constants.Configuration.DISABLE_SOAP_ACTION);

        if (!JavaUtils.isTrueExplicitly(disableSoapAction)) {
            // first try to get the SOAP action from message context
            soapActionString = messageContext.getSoapAction();
            if (log.isDebugEnabled()) {
                log.debug("SOAP Action from messageContext : (" + soapActionString + ")");
                
            }
            if (isGeneratedName(soapActionString)) {
                if (log.isDebugEnabled()) {
                    log.debug("Will not use SOAP Action because (" + soapActionString + ") was auto-generated");
                }
                soapActionString = null;
            }
            if ((soapActionString == null) || (soapActionString.length() == 0)) {
                // now let's try to get WSA action
                soapActionString = messageContext.getWSAAction();
                if (log.isDebugEnabled()) {
                    log.debug("SOAP Action from getWSAAction was : (" + soapActionString + ")");
                }
                if (messageContext.getAxisOperation() != null
                        && ((soapActionString == null) || (soapActionString
                        .length() == 0))) {
                    // last option is to get it from the axis operation
                    String axisOpSOAPAction = messageContext.getAxisOperation().
                        getSoapAction();
                    if (log.isDebugEnabled()) {
                        log.debug("SOAP Action from AxisOperation was : (" + axisOpSOAPAction + ")");
                    }
                    if (isGeneratedName(axisOpSOAPAction)) {
                        if (log.isDebugEnabled()) {
                            log.debug("Will not override SOAP Action because (" + axisOpSOAPAction + ") in AxisOperation was auto-generated");
                        }   
                    } else {
                        soapActionString = axisOpSOAPAction;
                    }   
                }
            }
        }

        //Since action is optional for SOAP 1.2 we can return null here.
        if (soapActionString == null && messageContext.isSOAP11()) {
            soapActionString = "\"\"";
        }

        return soapActionString;
    }
    
    public void setHTTPClientVersion(ConfigurationContext configurationContext) {
        configurationContext.setProperty(HTTPTransportConstants.HTTP_CLIENT_VERSION,
                HTTPTransportConstants.HTTP_CLIENT_5_X_VERSION);
    }
    
}
