/**
 * 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.cxf.ws.addressing.impl;


import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.namespace.QName;

import org.apache.cxf.Bus;
import org.apache.cxf.binding.soap.SoapBindingConstants;
import org.apache.cxf.binding.soap.model.SoapOperationInfo;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.StringUtils;
import org.apache.cxf.endpoint.ConduitSelector;
import org.apache.cxf.endpoint.Endpoint;
import org.apache.cxf.endpoint.NullConduitSelector;
import org.apache.cxf.endpoint.PreexistingConduitSelector;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.interceptor.InterceptorChain;
import org.apache.cxf.interceptor.OutgoingChainInterceptor;
import org.apache.cxf.io.DelegatingInputStream;
import org.apache.cxf.message.Exchange;
import org.apache.cxf.message.Message;
import org.apache.cxf.message.MessageUtils;
import org.apache.cxf.phase.PhaseInterceptorChain;
import org.apache.cxf.service.model.BindingFaultInfo;
import org.apache.cxf.service.model.BindingOperationInfo;
import org.apache.cxf.service.model.EndpointInfo;
import org.apache.cxf.service.model.Extensible;
import org.apache.cxf.service.model.FaultInfo;
import org.apache.cxf.service.model.MessageInfo;
import org.apache.cxf.transport.Conduit;
import org.apache.cxf.transport.ConduitInitiator;
import org.apache.cxf.transport.ConduitInitiatorManager;
import org.apache.cxf.transport.Destination;
import org.apache.cxf.transport.MessageObserver;
import org.apache.cxf.workqueue.OneShotAsyncExecutor;
import org.apache.cxf.workqueue.SynchronousExecutor;
import org.apache.cxf.workqueue.WorkQueueManager;
import org.apache.cxf.ws.addressing.AddressingProperties;
import org.apache.cxf.ws.addressing.AttributedURIType;
import org.apache.cxf.ws.addressing.ContextUtils;
import org.apache.cxf.ws.addressing.EndpointReferenceType;
import org.apache.cxf.ws.addressing.EndpointReferenceUtils;
import org.apache.cxf.ws.addressing.FaultAction;
import org.apache.cxf.ws.addressing.JAXWSAConstants;
import org.apache.cxf.ws.addressing.Names;



/**
 * Holder for utility methods relating to contexts.
 */
final class InternalContextUtils {
    private static final class DecoupledDestination implements Destination {
        private final EndpointInfo ei;
        private final EndpointReferenceType reference;

        private DecoupledDestination(EndpointInfo ei, EndpointReferenceType reference) {
            this.ei = ei;
            this.reference = reference;
        }

        public EndpointReferenceType getAddress() {
            return reference;
        }

        public Conduit getBackChannel(Message inMessage) throws IOException {
            if (ContextUtils.isNoneAddress(reference)) {
                return null;
            }
            Bus bus = inMessage.getExchange().get(Bus.class);
            //this is a response targeting a decoupled endpoint.   Treat it as a oneway so
            //we don't wait for a response.
            inMessage.getExchange().setOneWay(true);
            ConduitInitiator conduitInitiator 
                = bus.getExtension(ConduitInitiatorManager.class)
                    .getConduitInitiatorForUri(reference.getAddress().getValue());
            if (conduitInitiator != null) {
                Conduit c = conduitInitiator.getConduit(ei, reference, bus);
                // ensure decoupled back channel input stream is closed
                c.setMessageObserver(new MessageObserver() {
                    public void onMessage(Message m) {
                        InputStream is = m.getContent(InputStream.class);
                        if (is != null) {
                            try {
                                is.close();
                            } catch (Exception e) {
                                // ignore
                            }
                        }
                    }
                });
                return c;
            }
            return null;
        }

        public MessageObserver getMessageObserver() {
            return null;
        }

        public void shutdown() {
        }

        public void setMessageObserver(MessageObserver observer) {
        }
    }

    private static final Logger LOG = LogUtils.getL7dLogger(InternalContextUtils.class);

   /**
    * Prevents instantiation.
    */
    private InternalContextUtils() {
    }

    
    /**
     * Rebase response on replyTo
     * 
     * @param reference the replyTo reference
     * @param inMAPs the inbound MAPs
     * @param inMessage the current message
     */
    //CHECKSTYLE:OFF  Max executable statement count limitation
    public static void rebaseResponse(EndpointReferenceType reference,
                                      AddressingProperties inMAPs,
                                      final Message inMessage) {
        
        String namespaceURI = inMAPs.getNamespaceURI();
        if (!ContextUtils.retrievePartialResponseSent(inMessage)) {
            ContextUtils.storePartialResponseSent(inMessage);
            Exchange exchange = inMessage.getExchange();
            Message fullResponse = exchange.getOutMessage();
            Message partialResponse = ContextUtils.createMessage(exchange);
            ensurePartialResponseMAPs(partialResponse, namespaceURI);
            
            // ensure the inbound MAPs are available in the partial response
            // message (used to determine relatesTo etc.)
            ContextUtils.propogateReceivedMAPs(inMAPs, partialResponse);
            Destination target = inMessage.getDestination();
            if (target == null) {
                return;
            }
            
            try {
                if (reference == null) {
                    reference = ContextUtils.getNoneEndpointReference();
                }
                Conduit backChannel = target.getBackChannel(inMessage);
                Exception exception = inMessage.getContent(Exception.class);
                //Add this to handle two way faultTo
                //TODO:Look at how to refactor 
                if (backChannel != null && !inMessage.getExchange().isOneWay() 
                    && ContextUtils.isFault(inMessage)) {
                    // send the fault message to faultTo Endpoint
                    exchange.setOutMessage(ContextUtils.createMessage(exchange));
                    exchange.put(ConduitSelector.class, new NullConduitSelector());
                    exchange.put("org.apache.cxf.http.no_io_exceptions", true);
                    Destination destination = createDecoupledDestination(exchange, reference);
                    exchange.setDestination(destination);

                    if (ContextUtils.retrieveAsyncPostResponseDispatch(inMessage)) {
                        DelegatingInputStream in = inMessage.getContent(DelegatingInputStream.class);
                        if (in != null) {
                            in.cacheInput();
                        }
                        inMessage.getInterceptorChain().reset();
                        //cleanup pathinfo
                        if (inMessage.get(Message.PATH_INFO) != null) {
                            inMessage.remove(Message.PATH_INFO);
                        }
                        inMessage.getInterceptorChain().doIntercept(inMessage);

                    }

                    // send the partial response to requester
                    partialResponse.put("forced.faultstring",
                                        "The server sent HTTP status code :"
                                            + inMessage.getExchange().get(Message.RESPONSE_CODE));
                    partialResponse.setContent(Exception.class, exception);
                    partialResponse.put(org.apache.cxf.message.Message.PROTOCOL_HEADERS,
                                        inMessage.get(Message.PROTOCOL_HEADERS));
                    partialResponse.put(org.apache.cxf.message.Message.ENCODING,
                                        inMessage.get(Message.ENCODING));
                    partialResponse.put(ContextUtils.ACTION, inMessage.get(ContextUtils.ACTION));
                    partialResponse.put("javax.xml.ws.addressing.context.inbound",
                                        inMessage.get("javax.xml.ws.addressing.context.inbound"));
                    partialResponse.put("javax.xml.ws.addressing.context.outbound",
                                        inMessage.get("javax.xml.ws.addressing.context.outbound"));
                    exchange.setOutMessage(partialResponse);
                    PhaseInterceptorChain newChian = ((PhaseInterceptorChain)inMessage.getInterceptorChain())
                        .cloneChain();
                    partialResponse.setInterceptorChain(newChian);
                    exchange.setDestination(target);
                    exchange.setOneWay(false);
                    exchange.put(ConduitSelector.class,
                                 new PreexistingConduitSelector(backChannel, exchange.get(Endpoint.class)));
                    if (newChian != null && !newChian.doIntercept(partialResponse)
                        && partialResponse.getContent(Exception.class) != null) {
                        if (partialResponse.getContent(Exception.class) instanceof Fault) {
                            throw (Fault)partialResponse.getContent(Exception.class);
                        } else {
                            throw new Fault(partialResponse.getContent(Exception.class));
                        }
                    }
                    return;
                }
                
                if (backChannel != null) {
                    partialResponse.put(Message.PARTIAL_RESPONSE_MESSAGE, Boolean.TRUE);
                    partialResponse.put(Message.EMPTY_PARTIAL_RESPONSE_MESSAGE, Boolean.TRUE);
                    boolean robust =
                        MessageUtils.isTrue(inMessage.getContextualProperty(Message.ROBUST_ONEWAY));
                    
                    if (robust) {
                        BindingOperationInfo boi = exchange.get(BindingOperationInfo.class);
                        // insert the executor in the exchange to fool the OneWayProcessorInterceptor
                        exchange.put(Executor.class, getExecutor(inMessage));
                        // pause dispatch on current thread and resume...
                        inMessage.getInterceptorChain().pause();
                        inMessage.getInterceptorChain().resume();
                        MessageObserver faultObserver = inMessage.getInterceptorChain().getFaultObserver();
                        if (null != inMessage.getContent(Exception.class) && null != faultObserver) {
                            // return the fault over the response fault channel
                            inMessage.getExchange().setOneWay(false);
                            faultObserver.onMessage(inMessage);
                            return;
                        }
                        // restore the BOI for the partial response handling
                        exchange.put(BindingOperationInfo.class, boi);
                    }
                    
                    
                    // set up interceptor chains and send message
                    InterceptorChain chain =
                        fullResponse != null
                        ? fullResponse.getInterceptorChain()
                        : OutgoingChainInterceptor.getOutInterceptorChain(exchange);
                    exchange.setOutMessage(partialResponse);
                    partialResponse.setInterceptorChain(chain);
                    exchange.put(ConduitSelector.class,
                                 new PreexistingConduitSelector(backChannel,
                                                                exchange.get(Endpoint.class)));

                    if (chain != null && !chain.doIntercept(partialResponse) 
                        && partialResponse.getContent(Exception.class) != null) {
                        if (partialResponse.getContent(Exception.class) instanceof Fault) {
                            throw (Fault)partialResponse.getContent(Exception.class);
                        } else {
                            throw new Fault(partialResponse.getContent(Exception.class));
                        }
                    }
                    if (chain != null) {
                        chain.reset();                        
                    }
                    exchange.put(ConduitSelector.class, new NullConduitSelector());
                    
                    if (fullResponse == null) {
                        fullResponse = ContextUtils.createMessage(exchange);
                    }
                    exchange.setOutMessage(fullResponse);
                    
                    Destination destination = createDecoupledDestination(
                        exchange, 
                        reference);
                    exchange.setDestination(destination);
                         
                    
                    if (ContextUtils.retrieveAsyncPostResponseDispatch(inMessage) && !robust) {
                        //need to suck in all the data from the input stream as
                        //the transport might discard any data on the stream when this 
                        //thread unwinds or when the empty response is sent back
                        DelegatingInputStream in = inMessage.getContent(DelegatingInputStream.class);
                        if (in != null) {
                            in.cacheInput();
                        }
                        
                        // async service invocation required *after* a response
                        // has been sent (i.e. to a oneway, or a partial response
                        // to a decoupled twoway)
                        
                        //cleanup pathinfo
                        if (inMessage.get(Message.PATH_INFO) != null) {
                            inMessage.remove(Message.PATH_INFO);
                        }
                        // pause dispatch on current thread ...
                        inMessage.getInterceptorChain().pause();

                        try {
                            // ... and resume on executor thread
                            getExecutor(inMessage).execute(new Runnable() {
                                public void run() {
                                    inMessage.getInterceptorChain().resume();
                                }
                            });
                        } catch (RejectedExecutionException e) {
                            LOG.warning(
                                        "Executor queue is full, use the caller thread." 
                                        + "  Users can specify a larger executor queue to avoid this.");
                            // only block the thread if the prop is unset or set to false, otherwise let it go
                            if (!MessageUtils.isTrue(
                                inMessage.getContextualProperty(
                                    "org.apache.cxf.oneway.rejected_execution_exception"))) {
                                //the executor queue is full, so run the task in the caller thread
                                inMessage.getInterceptorChain().resume();
                            }
                        }
                    }
                } 
            } catch (Exception e) {
                LOG.log(Level.WARNING, "SERVER_TRANSPORT_REBASE_FAILURE_MSG", e);
            }
        }
    }
    //CHECKSTYLE:ON

    public static Destination createDecoupledDestination(
        Exchange exchange, final EndpointReferenceType reference) {
        final EndpointInfo ei = exchange.get(Endpoint.class).getEndpointInfo();
        return new DecoupledDestination(ei, reference);
    }
    
    /**
     * Construct and store MAPs for partial response.
     * 
     * @param partialResponse the partial response message
     * @param namespaceURI the current namespace URI
     */
    private static void ensurePartialResponseMAPs(Message partialResponse,
                                                 String namespaceURI) {
        // ensure there is a MAPs instance available for the outbound
        // partial response that contains appropriate To and ReplyTo
        // properties (i.e. anonymous & none respectively)
        AddressingProperties maps = new AddressingProperties();
        maps.setTo(EndpointReferenceUtils.getAnonymousEndpointReference());
        maps.setReplyTo(ContextUtils.WSA_OBJECT_FACTORY.createEndpointReferenceType());
        maps.getReplyTo().setAddress(ContextUtils.getAttributedURI(Names.WSA_NONE_ADDRESS));
        maps.setAction(ContextUtils.getAttributedURI(""));
        maps.exposeAs(namespaceURI);
        ContextUtils.storeMAPs(maps, partialResponse, true, true, false);
    }



    
    /**
     * Construct the Action URI.
     * 
     * @param message the current message
     * @return the Action URI
     */
    public static AttributedURIType getAction(Message message) {
        String action = null;
        LOG.fine("Determining action");
        Exception fault = message.getContent(Exception.class);

        if (fault instanceof Fault 
            && Names.WSA_NAMESPACE_NAME.equals(((Fault)fault).getFaultCode().getNamespaceURI())) {
            // wsa relevant faults should use the wsa-fault action value
            action = Names.WSA_DEFAULT_FAULT_ACTION;
        } else {
            FaultAction annotation = null;
            if (fault != null) {
                annotation = fault.getClass().getAnnotation(FaultAction.class);
            }
            if ((annotation != null) && (annotation.value() != null)) {
                action = annotation.value();
            } else {
                action = getActionFromServiceModel(message, fault);
            }
        }
        LOG.fine("action: " + action);
        return action != null ? ContextUtils.getAttributedURI(action) : null;
    }

    /**
     * Get action from service model.
     *
     * @param message the current message
     * @param fault the fault if one is set
     */
    private static String getActionFromServiceModel(Message message,
                                                    Exception fault) {
        String action = null;
        BindingOperationInfo bindingOpInfo =
            message.getExchange().get(BindingOperationInfo.class);
        if (bindingOpInfo != null) {
            if (bindingOpInfo.isUnwrappedCapable()) {
                bindingOpInfo = bindingOpInfo.getUnwrappedOperation();
            }
            if (fault == null) {
                action = (String)message.get(ContextUtils.ACTION);
                if (StringUtils.isEmpty(action)) {
                    action = (String) message.get(SoapBindingConstants.SOAP_ACTION);
                }
                if (action == null || "".equals(action)) {
                    MessageInfo msgInfo = 
                        ContextUtils.isRequestor(message)
                        ? bindingOpInfo.getOperationInfo().getInput()
                        : bindingOpInfo.getOperationInfo().getOutput();
                    String cachedAction = (String)msgInfo.getProperty(ContextUtils.ACTION);
                    if (cachedAction == null) {
                        action = getActionFromMessageAttributes(msgInfo);
                    } else {
                        action = cachedAction;
                    }
                    if (action == null && ContextUtils.isRequestor(message)) {
                        SoapOperationInfo soi = getSoapOperationInfo(bindingOpInfo);
                        action = soi == null ? null : soi.getAction();
                        action = StringUtils.isEmpty(action) ? null : action; 
                    }
                }
            } else {
                Throwable t = fault.getCause();
                
                // FaultAction attribute is not defined in 
                // http://www.w3.org/2005/02/addressing/wsdl schema
                for (BindingFaultInfo bfi : bindingOpInfo.getFaults()) {
                    FaultInfo fi = bfi.getFaultInfo();
                    if (fi.size() == 0) {
                        continue;
                    }
                    Class<?> fiTypeClass = fi.getFirstMessagePart().getTypeClass();
                    if (t != null 
                            && fiTypeClass != null
                            && t.getClass().isAssignableFrom(fiTypeClass)) {
                        if (fi.getExtensionAttributes() == null) {
                            continue;
                        }
                        String attr = (String)
                            fi.getExtensionAttributes().get(Names.WSAW_ACTION_QNAME);
                        if (attr == null) {
                            attr = (String)        
                                fi.getExtensionAttributes()
                                    .get(new QName(Names.WSA_NAMESPACE_WSDL_NAME_OLD,
                                                    Names.WSAW_ACTION_NAME));                            
                        }
                        if (attr != null) {
                            action = attr;
                            break;
                        }
                    }
                }
            }
        }
        LOG.fine("action determined from service model: " + action);
        return action;
    }

    public static SoapOperationInfo getSoapOperationInfo(BindingOperationInfo bindingOpInfo) {
        SoapOperationInfo soi = bindingOpInfo.getExtensor(SoapOperationInfo.class);
        if (soi == null && bindingOpInfo.isUnwrapped()) {
            soi = bindingOpInfo.getWrappedOperation()
                .getExtensor(SoapOperationInfo.class);
        }
        return soi;
    }

    /**
     * Get action from attributes on MessageInfo
     *
     * @param bindingOpInfo the current BindingOperationInfo
     * @param msgInfo the current MessageInfo
     * @return the action if set
     */
    private static String getActionFromMessageAttributes(MessageInfo msgInfo) {
        String action = null;
        if (msgInfo != null
            && msgInfo.getExtensionAttributes() != null) {
            String attr = getAction(msgInfo);
            if (!StringUtils.isEmpty(attr)) {
                action = attr;
                msgInfo.setProperty(ContextUtils.ACTION, action);
            }
        }
        return action;
    }

    public static String getAction(Extensible ext) {
        Object o = ext.getExtensionAttribute(JAXWSAConstants.WSAW_ACTION_QNAME);
        if (o == null) {
            o = ext.getExtensionAttributes().get(new QName(Names.WSA_NAMESPACE_WSDL_METADATA,
                                                           Names.WSAW_ACTION_NAME));
        }
        if (o == null) {
            o = ext.getExtensionAttributes().get(new QName(JAXWSAConstants.NS_WSA, Names.WSAW_ACTION_NAME));
        }
        if (o == null) {
            o = ext.getExtensionAttributes().get(new QName(Names.WSA_NAMESPACE_WSDL_NAME_OLD,
                                                   Names.WSAW_ACTION_NAME));
        }
        if (o instanceof QName) {
            return ((QName)o).getLocalPart();
        }
        return o == null ? null : o.toString();
    }

    /**
     * Get the Executor for this invocation.
     * @param endpoint
     * @return
     */
    private static Executor getExecutor(final Message message) {
        Endpoint endpoint = message.getExchange().get(Endpoint.class);
        Executor executor = endpoint.getService().getExecutor();
        
        if (executor == null || SynchronousExecutor.isA(executor)) {
            // need true asynchrony
            Bus bus = message.getExchange().get(Bus.class);
            if (bus != null) {
                WorkQueueManager workQueueManager =
                    bus.getExtension(WorkQueueManager.class);
                Executor autoWorkQueue =
                    workQueueManager.getNamedWorkQueue("ws-addressing");
                executor = autoWorkQueue != null
                           ? autoWorkQueue
                           :  workQueueManager.getAutomaticWorkQueue();
            } else {
                executor = OneShotAsyncExecutor.getInstance();
            }
        }
        message.getExchange().put(Executor.class, executor);
        return executor;
    }
 
}
