/*
 * 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.servicemix.components.http;

import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

import javax.jbi.JBIException;
import javax.jbi.component.ComponentContext;
import javax.jbi.messaging.DeliveryChannel;
import javax.jbi.messaging.ExchangeStatus;
import javax.jbi.messaging.InOnly;
import javax.jbi.messaging.InOut;
import javax.jbi.messaging.MessageExchange;
import javax.jbi.messaging.MessageExchangeFactory;
import javax.jbi.messaging.NormalizedMessage;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.XMLConstants;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMSource;

import org.apache.servicemix.components.util.ComponentSupport;
import org.apache.servicemix.jbi.jaxp.SourceTransformer;
import org.codehaus.xfire.DefaultXFire;
import org.codehaus.xfire.MessageContext;
import org.codehaus.xfire.XFire;
import org.codehaus.xfire.aegis.AegisBindingProvider;
import org.codehaus.xfire.attachments.Attachment;
import org.codehaus.xfire.attachments.Attachments;
import org.codehaus.xfire.fault.XFireFault;
import org.codehaus.xfire.service.Service;
import org.codehaus.xfire.service.invoker.BeanInvoker;
import org.codehaus.xfire.service.binding.ObjectServiceFactory;
import org.codehaus.xfire.soap.SoapConstants;
import org.codehaus.xfire.soap.handler.ReadHeadersHandler;
import org.codehaus.xfire.transport.http.XFireServletController;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

public class HttpSoapInOutBinding extends ComponentSupport implements
        HttpBinding {

    protected XFire xfire;
    protected XFireServletController controller;
    protected Service service;
    protected boolean defaultInOut = true;
    protected String soapAction = "\"\"";
    protected SourceTransformer transformer;

    public HttpSoapInOutBinding() {
    }
    
    public void init(ComponentContext context) throws JBIException {
        super.init(context);
        xfire = new DefaultXFire();
        ObjectServiceFactory factory = new ObjectServiceFactory(xfire.getTransportManager(),
                                                                new AegisBindingProvider());
        factory.setVoidOneWay(true);
        factory.setStyle(SoapConstants.STYLE_DOCUMENT);
        if (isDefaultInOut()) {
            service = factory.create(InOutService.class);
            service.setInvoker(new BeanInvoker(new InOutService(this)));
        } else {
            service = factory.create(InOnlyService.class);
            service.setInvoker(new BeanInvoker(new InOnlyService(this)));
        }
        xfire.getServiceRegistry().register(service);
        controller = new Controller(xfire);
        transformer = new SourceTransformer();
    }
    
    public void process(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        controller.doService(request, response);
    }

    public void invokeInOnly(Source source, MessageContext context) throws XFireFault {
        if (source == null) { 
            throw new XFireFault("Invalid source.", XFireFault.SENDER);
        }
        try {
        	if (soapAction != null) {
        		XFireServletController.getResponse().setHeader("SOAPAction", soapAction);
        	}
            DeliveryChannel channel = getDeliveryChannel();
            MessageExchangeFactory factory = channel.createExchangeFactory();
            InOnly exchange = factory.createInOnlyExchange();
            populateExchange(exchange, source, context);
            boolean result = channel.sendSync(exchange);
            if (!result) {
                throw new XFireFault("Error sending exchange", XFireFault.SENDER);
            }
        } catch (XFireFault e) {
            throw e;
        } catch (Exception e) {
            throw new XFireFault(e);
        }
    }
    
    public Source invokeInOut(Source source, MessageContext context) throws XFireFault {
        if (source == null) { 
            throw new XFireFault("Invalid source.", XFireFault.SENDER);
        }
        try {
        	if (soapAction != null) {
        		XFireServletController.getResponse().setHeader("SOAPAction", soapAction);
        	}
            DeliveryChannel channel = getDeliveryChannel();
            MessageExchangeFactory factory = channel.createExchangeFactory();
            InOut exchange = factory.createInOutExchange();
            populateExchange(exchange, source, context);
            boolean result = channel.sendSync(exchange);
            if (!result) {
                throw new XFireFault("Error sending exchange", XFireFault.SENDER);
            }
            if (exchange.getStatus() == ExchangeStatus.ERROR) {
                Exception e = exchange.getError();
                if (e == null) {
                    throw new XFireFault("Received error", XFireFault.SENDER);
                } else {
                    throw new XFireFault(e, XFireFault.SENDER);
                }
            }
            NormalizedMessage outMessage = exchange.getOutMessage();
            if (outMessage == null) {
                exchange.setError(new Exception("Expected an out message"));
                channel.sendSync(exchange);
                throw new XFireFault("No response", XFireFault.SENDER);
            }                    
            Source src = exchange.getOutMessage().getContent();
            exchange.setStatus(ExchangeStatus.DONE);
            channel.send(exchange);
            src = transformer.toDOMSource(src);
            return src;
        } catch (XFireFault e) {
            throw e;
        } catch (Exception e) {
            throw new XFireFault(e);
		}
    }
    
    protected void populateExchange(MessageExchange exchange, Source src, MessageContext ctx) throws Exception {
        // TODO: Retrieve properties
        NormalizedMessage inMessage = exchange.createMessage();
        // Add removed namespace declarations from the parents
        Map namespaces = (Map) ctx.getProperty(ReadHeadersHandler.DECLARED_NAMESPACES);
        Node node = transformer.toDOMNode(src);
        Element element;
        if (node instanceof Element) {
        	element = (Element) node;
        } else if (node instanceof Document) {
        	element = ((Document) node).getDocumentElement();
        } else {
        	throw new UnsupportedOperationException("Unable to handle nodes of type " + node.getNodeType());
        }
        // Copy embedded namespaces from the envelope into the body root
        for (Iterator it = namespaces.entrySet().iterator(); it.hasNext();) {
        	Entry entry = (Entry) it.next();
            if (element.getAttributes().getNamedItemNS(
            		XMLConstants.XMLNS_ATTRIBUTE_NS_URI, 
            		(String) entry.getKey()) == null) {
            	element.setAttributeNS(
            			XMLConstants.XMLNS_ATTRIBUTE_NS_URI,
            			XMLConstants.XMLNS_ATTRIBUTE + ":" + (String) entry.getKey(),
            			(String) entry.getValue());
            }
        }
        // Set the source
        inMessage.setContent(new DOMSource(element));
        // Retrieve attachments
        Attachments attachments = (Attachments) ctx.getInMessage().getAttachments();
        if (attachments != null) {
            for (Iterator it = attachments.getParts(); it.hasNext();) {
                Attachment part = (Attachment) it.next();
                inMessage.addAttachment(part.getId(), part.getDataHandler());
            }
        }
        exchange.setMessage(inMessage, "in");
    }
    
    public static class InOnlyService {
        private HttpSoapInOutBinding component;
        public InOnlyService() {}
        public InOnlyService(HttpSoapInOutBinding component) {
            this.component = component;
        }
        public void invokeInOnly(Source source, MessageContext context) throws XFireFault {
            this.component.invokeInOnly(source, context);
        }
    }
    
    public static class InOutService {
        private HttpSoapInOutBinding component;
        public InOutService() {}
        public InOutService(HttpSoapInOutBinding component) {
            this.component = component;
        }
        public Source invokeInOut(Source source, MessageContext context) throws XFireFault {
            return this.component.invokeInOut(source, context);
        }
    }

    
    public class Controller extends XFireServletController {
        public Controller(XFire xfire) {
            super(xfire);
        }
        protected String getService(HttpServletRequest request) {
            return service.getSimpleName();
        }        
    }

    // Properties
    //-------------------------------------------------------------------------
    public boolean isDefaultInOut() {
        return defaultInOut;
    }

    /**
     * Sets whether an InOut (the default) or an InOnly message exchange will be used by default.
     */
    public void setDefaultInOut(boolean defaultInOut) {
        this.defaultInOut = defaultInOut;
    }

	public String getSoapAction() {
		return soapAction;
	}

	public void setSoapAction(String soapAction) {
		this.soapAction = soapAction;
	}

}
