/*
 * 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.ode.axis2.httpbinding;

import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.methods.RequestEntity;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.httpclient.params.HttpParams;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ode.axis2.Properties;
import org.apache.ode.axis2.util.URLEncodedTransformer;
import org.apache.ode.axis2.util.UrlReplacementTransformer;
import org.apache.ode.bpel.epr.MutableEndpoint;
import org.apache.ode.bpel.iapi.PartnerRoleMessageExchange;
import org.apache.ode.utils.DOMUtils;
import org.apache.ode.utils.Namespaces;
import org.apache.ode.utils.wsdl.Messages;
import org.apache.ode.utils.wsdl.WsdlUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import javax.wsdl.Binding;
import javax.wsdl.BindingInput;
import javax.wsdl.BindingOperation;
import javax.wsdl.BindingOutput;
import javax.wsdl.Message;
import javax.wsdl.Operation;
import javax.wsdl.Part;
import javax.wsdl.extensions.UnknownExtensibilityElement;
import javax.wsdl.extensions.http.HTTPOperation;
import javax.wsdl.extensions.mime.MIMEContent;
import javax.xml.namespace.QName;
import java.io.UnsupportedEncodingException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class HttpMethodConverter {

    private static final String CONTENT_TYPE_TEXT_XML = "text/xml";
    private static final Log log = LogFactory.getLog(HttpMethodConverter.class);

    protected static final Messages msgs = Messages.getMessages(Messages.class);
    protected Binding binding;

    public HttpMethodConverter(Binding binding) {
        this.binding = binding;
    }

    public HttpMethod createHttpRequest(PartnerRoleMessageExchange odeMex, HttpParams params) throws UnsupportedEncodingException {
        Operation operation = odeMex.getOperation();
        BindingOperation bindingOperation = binding.getBindingOperation(operation.getName(), operation.getInput().getName(), operation.getOutput().getName());

        // message to be sent
        Element message = odeMex.getRequest().getMessage();
        Message msgDef = operation.getInput().getMessage();

        // base url
        String url = ((MutableEndpoint) odeMex.getEndpointReference()).getUrl();

        // extract part values into a map and check that all parts are assigned a value
        Map<String, Element> partElements = extractPartElements(msgDef, message);

        // http method type
        // the operation may override the verb, this is an extension for RESTful BPEL
        String verb = WsdlUtils.resolveVerb(binding, bindingOperation);

        // build the http method itself
        HttpMethod method = prepareHttpMethod(bindingOperation, verb, partElements, url, params);
        return method;
    }


    /**
     * create and initialize the http method.
     * Http Headers that may been passed in the params are not set in this method.
     * Headers will be automatically set by HttpClient.
     * See usages of HostParams.DEFAULT_HEADERS
     * See org.apache.commons.httpclient.HttpMethodDirector#executeMethod(org.apache.commons.httpclient.HttpMethod)
     */
    protected HttpMethod prepareHttpMethod(BindingOperation opBinding, String verb, Map<String, Element> partValues,
                                           final String rootUri, HttpParams params) throws UnsupportedEncodingException {
        if (log.isDebugEnabled()) log.debug("Preparing http request...");
        // convenience variables...
        BindingInput bindingInput = opBinding.getBindingInput();
        HTTPOperation httpOperation = (HTTPOperation) WsdlUtils.getOperationExtension(opBinding);
        MIMEContent content = WsdlUtils.getMimeContent(bindingInput.getExtensibilityElements());
        String contentType = content == null ? "" : content.getType();
        boolean useUrlEncoded = WsdlUtils.useUrlEncoded(bindingInput) || PostMethod.FORM_URL_ENCODED_CONTENT_TYPE.equalsIgnoreCase(contentType);
        boolean useUrlReplacement = WsdlUtils.useUrlReplacement(bindingInput);

        // the http method to be built and returned
        HttpMethod method = null;

        // the 4 elements the http method may be made of
        String relativeUri = httpOperation.getLocationURI();
        String queryPath = null;
        RequestEntity requestEntity;
        String encodedParams = null;

        // ODE supports uri template in both port and operation location.
        // so assemble the final url *before* replacement
        String completeUri = rootUri;
        if (StringUtils.isNotEmpty(relativeUri)) {
            completeUri = completeUri + (completeUri.endsWith("/") || relativeUri.startsWith("/") ? "" : "/") + relativeUri;
        }

        if (useUrlReplacement) {
            // insert part values in the url
            completeUri = new UrlReplacementTransformer().transform(completeUri, partValues);
        } else if (useUrlEncoded) {
            // encode part values
            encodedParams = new URLEncodedTransformer().transform(partValues);
        }

        // http-client api is not really neat
        // something similar to the following would save some if/else manipulations.
        // But we have to deal with it as-is.
        //
        //  method = new Method(verb);
        //  method.setRequestEnity(..)
        //  etc...
        if ("GET".equalsIgnoreCase(verb) || "DELETE".equalsIgnoreCase(verb)) {
            if ("GET".equalsIgnoreCase(verb)) {
                method = new GetMethod();
            } else if ("DELETE".equalsIgnoreCase(verb)) {
                method = new DeleteMethod();
            }
            if (useUrlEncoded) {
                queryPath = encodedParams;
            }

            // Let http-client manage the redirection
            // see org.apache.commons.httpclient.params.HttpClientParams.MAX_REDIRECTS
            // default is 100
            method.setFollowRedirects(true);
        } else if ("POST".equalsIgnoreCase(verb) || "PUT".equalsIgnoreCase(verb)) {

            if ("POST".equalsIgnoreCase(verb)) {
                method = new PostMethod();
            } else if ("PUT".equalsIgnoreCase(verb)) {
                method = new PutMethod();
            }

            // some body-building...
            if (useUrlEncoded) {
                requestEntity = new StringRequestEntity(encodedParams, PostMethod.FORM_URL_ENCODED_CONTENT_TYPE, method.getParams().getContentCharset());
            } else if (contentType.endsWith(CONTENT_TYPE_TEXT_XML)) {
                // get the part to be put in the body
                Part part = opBinding.getOperation().getInput().getMessage().getPart(content.getPart());
                Element partValue = partValues.get(part.getName());
                // if the part has an element name, we must take the first element
                if (part.getElementName() != null) {
                    partValue = DOMUtils.getFirstChildElement(partValue);
                }
                String xmlString = DOMUtils.domToString(partValue);
                requestEntity = new ByteArrayRequestEntity(xmlString.getBytes(), contentType);
            } else {
                // should not happen because of HttpBindingValidator, but never say never
                throw new IllegalArgumentException("Unsupported content-type!");
            }

            // cast safely, PUT and POST are subclasses of EntityEnclosingMethod
            final EntityEnclosingMethod enclosingMethod = (EntityEnclosingMethod) method;
            enclosingMethod.setRequestEntity(requestEntity);
            enclosingMethod.setContentChunked(params.getBooleanParameter(Properties.PROP_HTTP_REQUEST_CHUNK, false));

        } else {
            // should not happen because of HttpBindingValidator, but never say never
            throw new IllegalArgumentException("Unsupported HTTP method: " + verb);
        }

        // link params together
        method.getParams().setDefaults(params);
        method.setPath(completeUri); // assumes that the path is properly encoded (URL safe).
        method.setQueryString(queryPath);

        // headers
        setHttpRequestHeaders(method, partValues, opBinding.getOperation().getInput().getMessage(), opBinding.getBindingInput());
        return method;
    }

    /**
     * Go through the list of {@linkplain Namespaces.ODE_HTTP_EXTENSION_NS}{@code :header} elements included in the input binding. For each of them, set the HTTP Request Header with the static value defined by the attribute {@linkplain Namespaces.ODE_HTTP_EXTENSION_NS}{@code :value},
     *  or the part value mentionned in the attribute {@linkplain Namespaces.ODE_HTTP_EXTENSION_NS}{@code :part}.
     */
    public void setHttpRequestHeaders(HttpMethod method, Map<String, Element> partValues, Message inputMessage, BindingInput bindingInput) {
        Collection<UnknownExtensibilityElement> headerBindings = WsdlUtils.getHttpHeaders(bindingInput.getExtensibilityElements());
        for (Iterator<UnknownExtensibilityElement> iterator = headerBindings.iterator(); iterator.hasNext();) {
            Element binding = iterator.next().getElement();
            String headerName = binding.getAttribute("name");
            String partName = binding.getAttribute("part");
            String value = binding.getAttribute("value");

            String headerValue;
            if (StringUtils.isNotEmpty(partName)) {
                // get the part to be put in the body
                Part part = inputMessage.getPart(partName);
                Element partValue = partValues.get(part.getName());
                // if the part has an element name, we must take the first element
                if (part.getElementName() != null) partValue = DOMUtils.getFirstChildElement(partValue);
                headerValue = DOMUtils.domToString(partValue);
            } else if (StringUtils.isNotEmpty(value)) {
                headerValue = value;
            } else {
                String errMsg = "Invalid binding: missing attribute! Expecting " + new QName(Namespaces.ODE_HTTP_EXTENSION_NS, "part") + " or " + new QName(Namespaces.ODE_HTTP_EXTENSION_NS, "value");
                if (log.isErrorEnabled()) {
                    log.error(errMsg);
                }
                throw new RuntimeException(errMsg);
            }
            method.setRequestHeader(headerName, HttpClientHelper.replaceCRLFwithLWS(headerValue));
        }
    }


    protected Map<String, Element> extractPartElements(Message msgDef, Element message) {
        Map<String, Element> partValues = new HashMap<String, Element>();
        for (Iterator iterator = msgDef.getParts().values().iterator(); iterator.hasNext();) {
            Part part = (Part) iterator.next();
            Element partEl = DOMUtils.findChildByName(message, new QName(null, part.getName()));
            if (partEl == null)
                throw new IllegalArgumentException(msgs.msgOdeMessageMissingRequiredPart(part.getName()));
            partValues.put(part.getName(), partEl);
        }
        return partValues;
    }


    /**
     * Create the element to be associated with this part into the {@link org.apache.ode.bpel.iapi.Message}.
     * <br/>An element named with the part name will be returned. the content of this element depends on the part.
     * <p/>If the part has a non-null element name, a new element will be created and named accordingly then the text value is inserted in this new element.
     * <br/>else the given text content is simply set on the part element.
     *
     * @param part
     * @param textContent
     * @return an element named with the part name will be returned
     */
    public Element createPartElement(Part part, String textContent) {
        Document doc = DOMUtils.newDocument();
        Element partElement = doc.createElementNS(null, part.getName());
        if (part.getElementName() != null) {
            Element element = doc.createElementNS(part.getElementName().getNamespaceURI(), part.getElementName().getLocalPart());
            element.setTextContent(textContent);
            partElement.appendChild(element);
        } else {
            partElement.setTextContent(textContent);
        }
        return partElement;
    }


    /**
     * Create the element to be associated with this part into the {@link org.apache.ode.bpel.iapi.Message}.
     * <p/>If the part has a non-null element name, the bodyElement is simply appended.
     * Else if the bodyElement has a text content, the value is set to the message.
     * Else append all nodes of bodyElement to the returned element. Attributes are ignored.
     * <p/>
     * The name of the returned element is the part name.
     *
     * @param part
     * @param receivedElement
     * @return the element to insert "as is" to ODE message
     */
    public Element createPartElement(Part part, Element receivedElement) {
        Document doc = DOMUtils.newDocument();
        Element partElement = doc.createElementNS(null, part.getName());
        if (part.getElementName() != null) {
            partElement.appendChild(doc.importNode(receivedElement, true));
        } else {
            if (DOMUtils.isEmptyElement(receivedElement)) {
                // Append an empty text node.
                // Warning! setting an empty string with setTextContent has not effect. See javadoc.
                partElement.appendChild(doc.createTextNode(""));
            } else {
                // No need to make the distinction between simple and complex types, importNode will handle it
                // !!! Attributes are ignored
                for (int m = 0; m < receivedElement.getChildNodes().getLength(); m++) {
                    Node child = receivedElement.getChildNodes().item(m);
                    partElement.appendChild(doc.importNode(child, true));
                }
            }
        }
        return partElement;
    }

    /**
     * Process the HTTP Response Headers.
     * <p/>
     * First go through the list of {@linkplain Namespaces.ODE_HTTP_EXTENSION_NS}{@code :header} elements included in the output binding. For each of them, set the header value as the value of the message part.
     * <br/>Then add all HTTP headers as header part in the message. The name of the header would be the part name.
     *
     * @param odeMessage
     * @param method
     * @param messageDef
     * @param bindingOutput
     */
    public void extractHttpResponseHeaders(org.apache.ode.bpel.iapi.Message odeMessage, HttpMethod method, Message messageDef, BindingOutput bindingOutput) {
        Collection<UnknownExtensibilityElement> headerBindings = WsdlUtils.getHttpHeaders(bindingOutput.getExtensibilityElements());

        // iterate through the list of header bindings
        // and set the message parts accordingly
        for (Iterator<UnknownExtensibilityElement> iterator = headerBindings.iterator(); iterator.hasNext();) {
            Element binding = iterator.next().getElement();
            String partName = binding.getAttribute("part");
            String headerName = binding.getAttribute("name");

            Part part = messageDef.getPart(partName);
            if (StringUtils.isNotEmpty(partName)) {
                odeMessage.setPart(partName, createPartElement(part, method.getRequestHeader(headerName).getValue()));
            } else {
                String errMsg = "Invalid binding: missing required attribute! Part name: " + new QName(Namespaces.ODE_HTTP_EXTENSION_NS, "part");
                if (log.isErrorEnabled()) log.error(errMsg);
                throw new RuntimeException(errMsg);
            }
        }

        // also add all HTTP headers into the messade as header parts
        Header[] reqHeaders = method.getResponseHeaders();
        for (int i = 0; i < reqHeaders.length; i++) {
            Header h = reqHeaders[i];
            odeMessage.setHeaderPart(h.getName(), h.getValue());
        }
    }


}

