/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.ws.core.jaxws.handler;

// $Id:HandlerChainExecutor.java 710 2006-08-08 20:19:52Z thomas.diesler@jboss.com $

import java.util.ArrayList;
import java.util.List;

import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPPart;
import javax.xml.ws.WebServiceException;
import javax.xml.ws.handler.Handler;
import javax.xml.ws.handler.LogicalHandler;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.MessageContext.Scope;
import javax.xml.ws.handler.soap.SOAPMessageContext;

import org.jboss.logging.Logger;
import org.jboss.ws.core.CommonMessageContext;
import org.jboss.ws.core.jaxws.SOAPFaultHelperJAXWS;
import org.jboss.ws.core.soap.SOAPEnvelopeImpl;
import org.jboss.ws.metadata.umdm.EndpointMetaData;
import org.jboss.wsf.common.DOMWriter;

/**
 * Executes a list of JAXWS handlers.
 *
 * @author Thomas.Diesler@jboss.org
 * @since 06-May-2004
 */
public class HandlerChainExecutor
{
   private static Logger log = Logger.getLogger(HandlerChainExecutor.class);

   // The endpoint meta data
   private EndpointMetaData epMetaData;
   // The list of handlers 
   protected List<Handler> handlers = new ArrayList<Handler>();
   // The list of executed handlers
   protected List<Handler> executedHandlers = new ArrayList<Handler>();
   // The index of the first handler that returned false during processing
   protected int falseIndex = -1;
   // True if the current direction is outbound
   protected Boolean isOutbound;
   // True if this is for the server/endpoint side, used to determine client side specific
   // conformance requirements.
   private boolean serverSide;

   public HandlerChainExecutor(EndpointMetaData epMetaData, List<Handler> unsortedChain, boolean serverSide)
   {
      this.epMetaData = epMetaData;
      this.serverSide = serverSide;

      // Sort handler logical handlers first
      List<Handler> sortedChain = new ArrayList<Handler>();
      for (Handler handler : unsortedChain)
      {
         if (handler instanceof LogicalHandler)
            sortedChain.add(handler);
      }
      for (Handler handler : unsortedChain)
      {
         if ((handler instanceof LogicalHandler) == false)
            sortedChain.add(handler);
      }

      log.debug("Create a handler executor: " + sortedChain);
      for (Handler handler : sortedChain)
      {
         handlers.add(handler);
      }
   }

   /**
    * Indicates the end of lifecycle for a HandlerChain.
    */
   public void close(MessageContext msgContext)
   {
      log.debug("close");
      MessageContextJAXWS context = (MessageContextJAXWS)msgContext;
      for (int index = 1; index <= executedHandlers.size(); index++)
      {
         Handler currHandler = executedHandlers.get(executedHandlers.size() - index);
         try
         {
            context.setCurrentScope(Scope.HANDLER);
            currHandler.close(msgContext);
         }
         finally
         {
            context.setCurrentScope(Scope.APPLICATION);
         }
      }
   }

   public boolean handleMessage(MessageContext msgContext)
   {
      isOutbound = (Boolean)msgContext.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
      if (isOutbound == null)
         throw new IllegalStateException("Cannot find property: " + MessageContext.MESSAGE_OUTBOUND_PROPERTY);

      boolean doNext = true;

      if (handlers.size() > 0)
      {
         log.debug("Enter: handle" + (isOutbound ? "Out" : "In ") + "BoundMessage");

         int index = getFirstHandler();
         Handler currHandler = null;
         try
         {
            String lastMessageTrace = null;
            while (doNext && index >= 0)
            {
               currHandler = handlers.get(index);

               if (log.isTraceEnabled() && msgContext instanceof SOAPMessageContext)
               {
                  SOAPPart soapPart = ((SOAPMessageContext)msgContext).getMessage().getSOAPPart();
                  lastMessageTrace = traceSOAPPart("BEFORE handleRequest - " + currHandler, soapPart, lastMessageTrace);
               }

               doNext = handleMessage(currHandler, msgContext);

               if (log.isTraceEnabled() && msgContext instanceof SOAPMessageContext)
               {
                  SOAPPart soapPart = ((SOAPMessageContext)msgContext).getMessage().getSOAPPart();
                  lastMessageTrace = traceSOAPPart("AFTER handleRequest - " + currHandler, soapPart, lastMessageTrace);
               }

               if (doNext)
                  index = getNextIndex(index);
            }
         }
         catch (RuntimeException ex)
         {
            doNext = false;
            processHandlerFailure(ex);
         }
         finally
         {
            // we start at this index in the response chain
            if (doNext == false)
               falseIndex = index;

            log.debug("Exit: handle" + (isOutbound ? "Out" : "In ") + "BoundMessage with status: " + doNext);
         }
      }

      return doNext;
   }

   public boolean handleFault(MessageContext msgContext, Exception ex)
   {
      isOutbound = (Boolean)msgContext.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
      if (isOutbound == null)
         throw new IllegalStateException("Cannot find property: " + MessageContext.MESSAGE_OUTBOUND_PROPERTY);

      boolean doNext = true;

      if (handlers.size() > 0)
      {
         log.debug("Enter: handle" + (isOutbound ? "Out" : "In ") + "BoundFault");

         if (msgContext instanceof SOAPMessageContext)
         {
            SOAPMessageContext soapContext = (SOAPMessageContext)msgContext;
            SOAPMessage soapMessage = soapContext.getMessage();

            // If the message is not already a fault message then it is replaced with a fault message
            try
            {
               if (soapMessage == null || soapMessage.getSOAPBody().getFault() == null)
               {
                  soapMessage = SOAPFaultHelperJAXWS.exceptionToFaultMessage(ex);
                  soapContext.setMessage(soapMessage);
               }
            }
            catch (SOAPException se)
            {
               throw new WebServiceException("Cannot convert exception to fault message", ex);
            }
         }

         int index = getFirstHandler();
         
         Handler currHandler = null;
         try
         {
            String lastMessageTrace = null;
            while (doNext && index >= 0)
            {
               currHandler = handlers.get(index);

               if (log.isTraceEnabled() && msgContext instanceof SOAPMessageContext)
               {
                  SOAPPart soapPart = ((SOAPMessageContext)msgContext).getMessage().getSOAPPart();
                  lastMessageTrace = traceSOAPPart("BEFORE handleFault - " + currHandler, soapPart, lastMessageTrace);
               }

               doNext = handleFault(currHandler, msgContext);

               if (log.isTraceEnabled() && msgContext instanceof SOAPMessageContext)
               {
                  SOAPPart soapPart = ((SOAPMessageContext)msgContext).getMessage().getSOAPPart();
                  lastMessageTrace = traceSOAPPart("AFTER handleFault - " + currHandler, soapPart, lastMessageTrace);
               }

               index = getNextIndex(index);
            }
         }
         catch (RuntimeException rte)
         {
            doNext = false;
            processHandlerFailure(rte);
         }
         finally
         {
            log.debug("Exit: handle" + (isOutbound ? "Out" : "In ") + "BoundFault with status: " + doNext);
         }
      }

      return doNext;
   }

   private int getFirstHandler()
   {
      int index;
      if (falseIndex == -1)
      {
         index = (isOutbound ? 0 : handlers.size() - 1);
      }
      else
      {
         index = getNextIndex(falseIndex);
      }
      return index;
   }

   private int getNextIndex(int prevIndex)
   {
      int nextIndex = (isOutbound ? prevIndex + 1 : prevIndex - 1);
      if (nextIndex >= handlers.size())
         nextIndex = -1;
      return nextIndex;
   }

   // 4.14 Conformance (Exceptions During Handler Processing): Exceptions thrown during handler processing on
   // the client MUST be passed on to the application. If the exception in question is a subclass of WebService-
   // Exception then an implementation MUST rethrow it as-is, without any additional wrapping, otherwise it
   // MUST throw a WebServiceException whose cause is set to the exception that was thrown during handler processing.
   private void processHandlerFailure(RuntimeException ex)
   {
      log.error("Exception during handler processing", ex);
      
      // If this call is server side then the conformance requirement specific to
      // clients can be avoided.
      if (serverSide == true)
      {
         throw ex;
      }
      
      if (ex instanceof WebServiceException)
      {
         throw (WebServiceException)ex;
      }
      throw new WebServiceException(ex);
   }

   private boolean handleMessage(Handler currHandler, MessageContext msgContext)
   {
      CommonMessageContext context = (CommonMessageContext)msgContext;
      if (currHandler instanceof LogicalHandler)
      {
         if (msgContext instanceof SOAPMessageContextJAXWS)
            msgContext = new LogicalMessageContextImpl((SOAPMessageContextJAXWS)msgContext);
      }

      if (executedHandlers.contains(currHandler) == false)
         executedHandlers.add(currHandler);

      try
      {
         context.put(MessageContextJAXWS.ALLOW_EXPAND_TO_DOM, Boolean.TRUE);
         context.setCurrentScope(Scope.HANDLER);
         return currHandler.handleMessage(msgContext);
      }
      finally
      {
         context.setCurrentScope(Scope.APPLICATION);
         context.remove(MessageContextJAXWS.ALLOW_EXPAND_TO_DOM);
      }
   }

   private boolean handleFault(Handler currHandler, MessageContext msgContext)
   {
      CommonMessageContext context = (CommonMessageContext)msgContext;
      if (currHandler instanceof LogicalHandler)
      {
         if (msgContext instanceof SOAPMessageContextJAXWS)
            msgContext = new LogicalMessageContextImpl((SOAPMessageContextJAXWS)msgContext);
      }

      if (executedHandlers.contains(currHandler) == false)
         executedHandlers.add(currHandler);

      try
      {
         context.put(MessageContextJAXWS.ALLOW_EXPAND_TO_DOM, Boolean.TRUE);
         context.setCurrentScope(Scope.HANDLER);
         return currHandler.handleFault(msgContext);
      }
      finally
      {
         context.setCurrentScope(Scope.APPLICATION);
         context.remove(MessageContextJAXWS.ALLOW_EXPAND_TO_DOM);
      }
   }

   /**
    * Trace the SOAPPart, do nothing if the String representation is equal to the last one.
    */
   protected String traceSOAPPart(String logMsg, SOAPPart soapPart, String lastMessageTrace)
   {
      try
      {
         SOAPEnvelopeImpl soapEnv = (SOAPEnvelopeImpl)soapPart.getEnvelope();
         String envString = DOMWriter.printNode(soapEnv, true);
         if (envString.equals(lastMessageTrace))
         {
            log.trace(logMsg + ": unchanged");
         }
         else
         {
            log.trace(logMsg + "\n" + envString);
            lastMessageTrace = envString;
         }
         return lastMessageTrace;
      }
      catch (SOAPException e)
      {
         log.error("Cannot get SOAPEnvelope", e);
         return null;
      }
   }
}
