/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 * 
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License. You can obtain
 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

/*
 * MessageExchangeHelper.java
 *
 * Created on February 5, 2006, 9:51 AM
 *
 * To change this template, choose Tools | Options and locate the template under
 * the Source Creation and Management node. Right-click the template and choose
 * Open. You can then make changes to the template in the Source Editor.
 */

package com.sun.enterprise.jbi.serviceengine.util.soap;

import org.w3c.dom.*;

import com.sun.enterprise.deployment.ServiceReferenceDescriptor;
import com.sun.enterprise.deployment.ServiceRefPortInfo;
import com.sun.enterprise.jbi.serviceengine.ServiceEngineException;
import com.sun.enterprise.jbi.serviceengine.util.JBIConstants;
import com.sun.enterprise.jbi.serviceengine.handlers.JBIHandler;
import com.sun.enterprise.jbi.serviceengine.handlers.JBIHandlerFactory;
import com.sun.enterprise.jbi.serviceengine.comm.MessageSender;
import com.sun.enterprise.jbi.serviceengine.comm.DefaultMessageExchangeTransport;
import com.sun.enterprise.jbi.serviceengine.comm.MessageExchangeTransport;
import com.sun.enterprise.jbi.serviceengine.core.JavaEEServiceEngineContext;
import com.sun.enterprise.jbi.serviceengine.core.ServiceEngineEndpoint;
import com.sun.enterprise.jbi.serviceengine.core.EndpointRegistry;
import com.sun.jbi.wsdl11wrapper.*;
import com.sun.logging.LogDomains;
import javax.jbi.messaging.*;
import javax.jbi.messaging.Fault;
import javax.jbi.servicedesc.ServiceEndpoint;
import javax.xml.namespace.QName;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPPart;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.wsdl.*;
import javax.wsdl.factory.*;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.util.concurrent.ConcurrentHashMap;
import java.util.List;
import java.net.URL;

/**
 * This class helps in normalizing a SOAP message and denormalizing a normalized
 * message into a SOAP Message
 * @author mu125243
 */
public class MessageExchangeHelper {
    
    private static Logger logger =
            LogDomains.getLogger(LogDomains.SERVER_LOGGER);
    
    private MessageExchange messageExchange;
    private URL wsdlLocation;
    private QName serviceName;
    private String endpointName;
    private List<JBIHandler> handlers; 

    // For storing the WSDL cache.
    private static ConcurrentHashMap<String, EndpointMetaData> wsdlCache =
                    new ConcurrentHashMap<String, EndpointMetaData>(11,0.75f,4);

    private static String WSDL11 = "http://schemas.xmlsoap.org/wsdl/";

    /** Creates a new instance of MessageExchangeHelper */
    public MessageExchangeHelper() {
        handlers = JBIHandlerFactory.getInstance().getHandlers();
    }
    
    public void setMessageExchange(MessageExchange messageExchange) {
        this.messageExchange = messageExchange;
    }
    
    
    public SOAPMessage denormalizeMessage(boolean inFlag) throws ServiceEngineException {
        validateMessageExchange();
        NormalizedMessage normalizedMsg = null;
        
        if(inFlag) {
            if(isInOutMessageExchange()) {
                InOut inOutExchange = (InOut)messageExchange;
                normalizedMsg = inOutExchange.getInMessage();
                
            } else {
                InOnly inOnlyExchange = (InOnly)messageExchange;
                normalizedMsg = inOnlyExchange.getInMessage();
            }
        } else {
            // assumed that it's a inout message
            normalizedMsg = (messageExchange.getFault()!=null)? 
                    messageExchange.getFault() : ((InOut)messageExchange).getOutMessage();
        //create inonly or inout message exchange based on the instance
        }
        
        DefaultMessageExchangeTransport meTransport = 
                new DefaultMessageExchangeTransport(messageExchange);
        meTransport.setMessage(normalizedMsg);
        invokeHandlersForOutbound(meTransport);

        QName operationQName = messageExchange.getOperation();
        String pattern = messageExchange.getPattern().toString();
        
        Operation operation = new Operation(operationQName.getLocalPart(), pattern);
        // DeNormalize response msg to SOAP msg
        
        MessageDenormalizerImpl d = new MessageDenormalizerImpl();
        SOAPWrapper wrapper;
        if(messageExchange.getFault()!=null) {
            // Assuming soap binding does not wrap a fault message
            wrapper = d.denormalizeFaultMessage((Fault)normalizedMsg);
        } else {
            unWrapMessage(normalizedMsg, inFlag);
            wrapper = d.denormalizeMessage(normalizedMsg, operation, !inFlag);
        }
        SOAPMessage message = wrapper.getMessage();
        printSOAPMessage( "Denormalizing in ? "+ inFlag + "message :" , message) ;
        return message;
        
    }
    
    public boolean isInOutMessageExchange() {
        return messageExchange instanceof InOut;
    }
    
    public void normalizeMessage(SOAPMessage soapMessage, boolean inFlag)  throws ServiceEngineException  {
        validateMessageExchange();
        if(soapMessage != null) {
            
            
            printSOAPMessage( "normalizing in ? "+ inFlag + "message :" , soapMessage) ;
            NormalizedMessage normalizedMsg = null;
            try {
                
                boolean isFault = (soapMessage.getSOAPBody().getFault() != null);
                normalizedMsg = 
                        (isFault)? messageExchange.createFault() : messageExchange.createMessage();
                
                //soapMessage.writeTo(System.out);
                if(isFault) {
                    // Assuming soap binding does not unwrap a fault message
                    SOAPWrapper wrapper = new SOAPWrapper(soapMessage);
                    MessageNormalizerImpl normalizer = new MessageNormalizerImpl();
                    normalizer.normalizeFaultMessage(wrapper, normalizedMsg);
                } else {
                    //normalizer.normalizeMessage(wrapper, normalizedMsg, operation);
                    normalizeAndWrapMessage(normalizedMsg, soapMessage, !inFlag);
                }
                
                if(isFault)
                    messageExchange.setFault((javax.jbi.messaging.Fault)normalizedMsg);
                else if(inFlag) {
                    if(isInOutMessageExchange()) {
                        ((InOut)messageExchange).setInMessage(normalizedMsg);
                    } else {
                        ((InOnly)messageExchange).setInMessage(normalizedMsg);
                    }
                } else // inout assumed.
                    ((InOut)messageExchange).setOutMessage(normalizedMsg);
                DefaultMessageExchangeTransport meTransport = 
                        new DefaultMessageExchangeTransport(messageExchange);
                meTransport.setMessage(normalizedMsg);
                invokeHandlersForInbound(meTransport);
            } catch(Exception e) {
                e.printStackTrace();
            }
            
        }
        
    }
    
    private void unWrapMessage(NormalizedMessage normalizedMsg, boolean server) {
        try {
            String endpointName = null;
            QName serviceName = null;
            EndpointMetaData emd = null;

            ServiceEndpoint serviceEndpoint = messageExchange.getEndpoint();
            if(serviceEndpoint != null) {
                EndpointRegistry endpointRegistry = EndpointRegistry.getInstance();
                endpointName = serviceEndpoint.getEndpointName();
                serviceName = serviceEndpoint.getServiceName();
                ServiceEngineEndpoint serviceEngineEndpoint = endpointRegistry.get(
                        serviceName, endpointName);
                if(serviceEngineEndpoint != null && server)
                    emd = serviceEngineEndpoint.getEndpointMetaData();
            }
            if(!server && wsdlLocation != null) {
                String wsdl = wsdlLocation.toURI().toString();
                serviceName = this.serviceName;
                endpointName = this.endpointName;
                emd  = getEndPointMetaData(wsdl, serviceName, endpointName);
            }

            // can emd be null?
            Definition mDefinition = emd.getDefinition();
            if(isWsdl11(mDefinition)) {
                Wsdl11WrapperHelper helper = new Wsdl11WrapperHelper(mDefinition);
                Source source = normalizedMsg.getContent();
                String operationName = messageExchange.getOperation().getLocalPart();
                boolean isProvider = messageExchange.getRole().equals(MessageExchange.Role.PROVIDER);
                Document unwrappedDoc = helper.unwrapMessage(source, 
                                                            serviceName,
                                                            endpointName,
                                                            operationName,
                                                            isProvider);
                normalizedMsg.setContent(new DOMSource(unwrappedDoc));
            }
        }catch(Exception e) {
            e.printStackTrace();
        }        
    }
    
    private void normalizeAndWrapMessage(NormalizedMessage normalizedMsg, 
                                         SOAPMessage soapMessage,
                                         boolean server) {
        try {
            String endpointName = null;
            QName serviceName = null;
            EndpointMetaData emd = null;
            ServiceEndpoint serviceEndpoint = messageExchange.getEndpoint();
            if(serviceEndpoint != null) {
                EndpointRegistry endpointRegistry = EndpointRegistry.getInstance();
                endpointName = serviceEndpoint.getEndpointName();
                serviceName = serviceEndpoint.getServiceName();
                ServiceEngineEndpoint serviceEngineEndpoint = endpointRegistry.get(
                serviceName, endpointName);
                if(serviceEngineEndpoint != null && server)
                    emd = serviceEngineEndpoint.getEndpointMetaData();
            }
            if(!server && wsdlLocation != null) {
                String wsdl = wsdlLocation.toURI().toString();
                serviceName = this.serviceName;
                endpointName = this.endpointName;
                emd  = getEndPointMetaData(wsdl, serviceName, endpointName);
            }

            Definition mDefinition = emd.getDefinition();
            Wsdl11WrapperHelper helper = new Wsdl11WrapperHelper(mDefinition);
            if(isWsdl11(mDefinition)) {
                //String operationName = messageExchange.getOperation().getLocalPart();
                String operationName = null;
                Operation operation = null;
                if (messageExchange.getOperation() == null) {
                    operationName = emd.getOperationName(soapMessage);
		    if (logger.isLoggable(Level.FINEST))
                        logger.finest("Operation Name is :" + operationName);
                    QName opQName = new QName(
                                    messageExchange.getService().getNamespaceURI(), 
                                    operationName);
                    messageExchange.setOperation(opQName);
                    operation = getOperation(soapMessage, 
                                    serviceName, operationName);
                } else {
                    operationName = messageExchange.getOperation().getLocalPart();
                    operation = new Operation(operationName,
                                    messageExchange.getPattern().toString());
                }

                SOAPWrapper wrapper = new SOAPWrapper(soapMessage);
                MessageNormalizerImpl normalizer = new MessageNormalizerImpl();
                normalizer.normalizeMessage(wrapper, normalizedMsg, operation);
                Document wrappedDoc = null;
                Source source = normalizedMsg.getContent();
                boolean isProvider = messageExchange.getRole().equals(MessageExchange.Role.PROVIDER);
                wrappedDoc = helper.wrapMessage(source, serviceName, endpointName, operationName,!isProvider);

                normalizedMsg.setContent(new DOMSource(wrappedDoc));
             }
        }catch(Exception e) {
            e.printStackTrace();
        }        
        
    }
    
    private Definition getWsdlDefinition(String wsdl) throws Exception{
        javax.wsdl.factory.WSDLFactory mFactory = WSDLFactory.newInstance();
        javax.wsdl.xml.WSDLReader mReader = mFactory.newWSDLReader();
        return mReader.readWSDL(wsdl);
    }

    private boolean isWsdl11(Definition mDefinition) 
    throws Wsdl11WrapperHelperException {
       try
       {
           if (mDefinition != null) {
               String xmlns = mDefinition.getNamespace ("");
               if (xmlns.trim ().equals(WSDL11)) {
                   return true;
               }
           }
       }
       catch (Exception e)
       {
           throw new Wsdl11WrapperHelperException("Cannot get version", e);
       }
       return false;
    } 

    public MessageExchange getMessageExchange() {
        return messageExchange;
    }

    /** This will only be called during JAX-RPC invocation */
    public void initializeMessageExchange(ServiceRefPortInfo portInfo, boolean oneWay)
    throws ServiceEngineException {
        ServiceReferenceDescriptor serviceRef = portInfo.getServiceReference();
        QName serviceName = serviceRef.getServiceName();
        String endpointName = portInfo.hasWsdlPort()? 
                                        portInfo.getWsdlPort().getLocalPart() : 
                                        portInfo.getName();
        URL wsdlFileUrl = serviceRef.getWsdlFileUrl();
        initializeMessageExchange(wsdlFileUrl, serviceName, endpointName, oneWay);
    }
    
    public void initializeMessageExchange(URL wsdlLocation, 
                                          QName service,
                                          String endpointName,
                                          boolean oneWay)
    throws ServiceEngineException {
        try {
            this.wsdlLocation = wsdlLocation;
            this.serviceName = service;
            this.endpointName = endpointName;
            DeliveryChannel channel =
                    JavaEEServiceEngineContext.getInstance(). getDeliveryChannel();
            // Create MessageExchange
            MessageExchangeFactory factory =
                    channel.createExchangeFactoryForService(serviceName);
            
            MessageExchange msgExchange = null;
            NormalizedMessage inMsg = null;
            
            if(oneWay) {
                InOnly inMessageExchange =  factory.createInOnlyExchange();
                inMsg = inMessageExchange.createMessage();
                inMessageExchange.setInMessage(inMsg);
                msgExchange = inMessageExchange;
            } else {
                InOut inOutMessageExchange =  factory.createInOutExchange();
                inMsg = inOutMessageExchange.createMessage();
                inOutMessageExchange.setInMessage(inMsg);
                msgExchange = inOutMessageExchange;
            }
            msgExchange.setService(serviceName);
            setMessageExchange(msgExchange);
        } catch(Exception e) {
            throw new ServiceEngineException(e);
        }
    }
    
    public void handleException(Exception exception) {
        try { 
            messageExchange.setStatus(ExchangeStatus.ERROR);
            if((messageExchange instanceof  InOut) || 
               (messageExchange instanceof RobustInOnly)){
                normalizeException(exception);
            }
            dispatchMessage();
        } catch(Exception e) {
           logger.log(Level.SEVERE, e.getMessage());
        }
    }
    
    public void handleResponse(SOAPMessage response, boolean flag) {
        try {
            if(messageExchange instanceof  InOut)  {
                normalizeMessage(response, flag);
            } else if((messageExchange instanceof InOnly ) ||
                (messageExchange instanceof RobustInOnly)) {
                messageExchange.setStatus(ExchangeStatus.DONE);
            }
            dispatchMessage();
        } catch(Exception e) {
           logger.log(Level.SEVERE, e.getMessage());
        }
    }
    
    public void normalizeException(Exception exception) {
        if(exception != null) {
            try {
                MessageDenormalizerImpl d = new MessageDenormalizerImpl();
                SOAPWrapper soapWrapper = d.denormalizeMessage(exception);
                normalizeMessage(soapWrapper.getMessage(), false);
                
            } catch(Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    public void dispatchMessage() throws ServiceEngineException {
        if(messageExchange != null) {
            MessageSender messageSender = new MessageSender();
            messageSender.setMessageExchange(messageExchange);
            messageSender.send();
            Exception exception = messageSender.getException();
            if(exception != null)
                throw new ServiceEngineException(exception);
        }
    }
    
    private void validateMessageExchange() throws ServiceEngineException {
        if(messageExchange == null)
            throw new ServiceEngineException("MessageExchange not set, use setMessageExchange()");
    }
    
    
    private Operation getOperation(SOAPMessage soapMessage, QName svcQName, 
        String opName) throws ServiceEngineException {
        try {
            // Get operation name from the localpart of the first child
            // of <env:Body> in SOAP msg
            SOAPPart sp = soapMessage.getSOAPPart();
            SOAPEnvelope env = sp.getEnvelope();
            SOAPBody body = env.getBody();
             // first child of body is like <ns0:sayHello>
            org.w3c.dom.Node firstChild = body.getFirstChild();
            String namespacePrefix = firstChild.getPrefix();
            
            // Get WSDL operation QName. This is in the same namespace as the
            // service, declared in the <definitions> element in the WSDL file.
            String svcNamespace = svcQName.getNamespaceURI();
            
            // namespace URI for body content is the WSDL "types" namespace
            // as in the <schema> element in the WSDL file.
            String namespaceURI = null;
            if(namespacePrefix != null)
                namespaceURI = env.getNamespaceURI(namespacePrefix);
            else
                namespaceURI = svcNamespace;
            
            // Normalize Message
            Operation operation = new Operation(opName, "in-out");
            // TODO Does JAXRPC2.0 allow WSDL 2.0's uri or multipart styles ?
            operation.setStyle("rpc");
            operation.setInputNamespace(namespaceURI);
            operation.setOutputNamespace(namespaceURI);
            return operation;
            
        } catch(Exception e) {
            e.printStackTrace();
            
            throw new ServiceEngineException(e.getMessage());
        }
    }
    
    private void invokeHandlersForInbound(MessageExchangeTransport meTransport) 
            throws ServiceEngineException {
        for (JBIHandler handler : handlers)
            try {
                handler.handleInbound(meTransport);
            } catch (Exception e) {
                logger.log(Level.SEVERE, e.getMessage(), e);
                throw new ServiceEngineException(e);
            }
    }
    
    private void invokeHandlersForOutbound(MessageExchangeTransport meTransport) 
            throws ServiceEngineException {
        for (JBIHandler handler : handlers)
            try {
                handler.handleOutbound(meTransport);
            } catch (Exception e) {
                logger.log(Level.SEVERE, e.getMessage(), e);
                throw new ServiceEngineException(e);
            }
    }

    /**This is the client side WSDL cache. The Provider cache is stored in 
     * ServiceEngineEndpoint. Client caching can be disabled using a System
     * property.*/
    private EndpointMetaData getEndPointMetaData(String wsdlPath, 
                                                 QName serviceName,
                                                 String epName) 
            throws Exception {
        String clientCache = System.getProperty(JBIConstants.CLIENT_CACHE);
        if("false".equalsIgnoreCase(clientCache)) {
            return createEndpointMetaData(wsdlPath, serviceName, epName);
        } else {
            EndpointMetaData emd = wsdlCache.get(wsdlPath);
            if(emd == null) {
                emd = createEndpointMetaData(wsdlPath, serviceName, epName);
                wsdlCache.put(wsdlPath, emd);
            }
            return emd;
        }
    }
    
    private EndpointMetaData createEndpointMetaData(String wsdlPath,
                                                    QName serviceName,
                                                    String epName)
            throws Exception {
        EndpointMetaData emd = new EndpointMetaData(getWsdlDefinition(wsdlPath), 
                                                    serviceName, 
                                                    epName);
        emd.resolve();
        return emd;
    }
    
    protected void printSOAPMessage(String message, SOAPMessage soapMessage) {
        try {
            if(logger.isLoggable(Level.FINE)) {
                System.out.print(message);
                soapMessage.writeTo(System.out);
            }
        }catch(Exception e) {
            System.out.println(e.getMessage());
        }
    }
    
    protected void printSOAPContent(String message, NormalizedMessage normalizedMessage) {
        if(logger.isLoggable(Level.FINE)) {
            if(normalizedMessage != null) {
                javax.xml.transform.Source source = normalizedMessage.getContent() ;
                if(source != null) {
                    try {
                        javax.xml.transform.Transformer transformer = javax.xml.transform.TransformerFactory.newInstance().newTransformer();
                        System.out.print(message);
                        javax.xml.transform.stream.StreamResult result = new javax.xml.transform.stream.StreamResult(System.out);
                        transformer.transform(source, result);
                    } catch(Exception e) {
                        e.printStackTrace();
                    }
                }
                
            }
        }
    }
    
}
