/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file 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.web.tomcat.service.session;

import java.io.IOException;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletException;

import org.apache.catalina.*;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.util.LifecycleSupport;
import org.apache.catalina.valves.ValveBase;
import org.jboss.logging.Logger;
import org.jboss.web.tomcat.service.session.distributedcache.spi.OutgoingDistributableSessionData;

/**
 * Web request valve to specifically handle Tomcat jvmRoute using mod_jk(2)
 * module. We assume that the session id has a format of id.jvmRoute where 
 * jvmRoute is used by JK module to determine sticky session during 
 * load balancing. Checks for failover by matching session and request jvmRoute
 * to the session manager's, updating the session and session cookie if a 
 * failover is detected.
 *
 * This valve is inserted in the pipeline only when mod_jk is configured.
 * 
 * @author Ben Wang
 * @author Brian Stansberry
 * 
 * @version $Revision: 87314 $
 */
public class JvmRouteValve extends ValveBase implements Lifecycle
{
   // The info string for this Valve
   private static final String info = "JvmRouteValve/1.0";

   protected static Logger log_ = Logger.getLogger(JvmRouteValve.class);

   // Valve-lifecycle_ helper object
   protected LifecycleSupport support = new LifecycleSupport(this);

   protected AbstractJBossManager manager_;

   /**
    * Create a new Valve.
    *
    */
   public JvmRouteValve(AbstractJBossManager manager)
   {
      super();
      manager_ = manager;
   }

   /**
    * Get information about this Valve.
    */
   public String getInfo()
   {
      return info;
   }

   public void invoke(Request request, Response response) throws IOException, ServletException
   {
      // Need to check it before let request through.
      checkJvmRoute(request, response);

      // let the servlet invocation go through
      getNext().invoke(request, response);
   }

   public void checkJvmRoute(Request req, Response res)
      throws IOException, ServletException
   {
      String requestedId = req.getRequestedSessionId();
      HttpSession session = req.getSession(false);
      if (session != null)
      {
         String sessionId = session.getId();

         // Obtain JvmRoute
         // FIXME do this once in constructor
         String jvmRoute = manager_.getJvmRoute();
         if (log_.isTraceEnabled())
         {
            log_.trace("checkJvmRoute(): check if need to re-route based on JvmRoute. Session id: " +
               sessionId + " jvmRoute: " + jvmRoute);
         }
         
         // FIXME do this once in the session manager's start()
         if (jvmRoute == null)
         {
            throw new RuntimeException("JvmRouteValve.checkJvmRoute(): Tomcat JvmRoute is null. " +
               "Need to assign a value in Tomcat server.xml for load balancing.");
         }

         // Check if incoming session id has JvmRoute appended. If not, append it.
         boolean setCookie = !req.isRequestedSessionIdFromURL();
         handleJvmRoute(requestedId, sessionId, jvmRoute, res, setCookie);
      }
   }

   protected void handleJvmRoute(String requestedId,
                                 String sessionId, 
                                 String jvmRoute, 
                                 HttpServletResponse response,
                                 boolean setCookie)
         throws IOException
   {      
      // The new id we'll give the session if we detect a failover
      String newId = null;
      
      // First, check if the session object's jvmRoute matches ours      
      
      // TODO. The current format is assumed to be id.jvmRoute. Can be generalized later.
      String sessionJvmRoute = null;
      int index = sessionId.indexOf('.', 0);
      if (index > -1 && index < sessionId.length() -1)
      {
         sessionJvmRoute = sessionId.substring(index + 1, sessionId.length());
      }
      
      if (!jvmRoute.equals(sessionJvmRoute))
      {
         if (index < 0)
         {
            // If this valve is turned on, we assume we have an appendix of jvmRoute. 
            // So this request is new.
            newId = new StringBuilder(sessionId).append('.').append(jvmRoute).toString();
         }         
         else 
         {
            // We just had a failover since jvmRoute does not match. 
            // We will replace the old one with the new one.         
            if (log_.isTraceEnabled())
            {
               log_.trace("handleJvmRoute(): We have detected a failover with different jvmRoute." +
                  " old one: " + sessionJvmRoute + " new one: " + jvmRoute + ". Will reset the session id.");
            }
            
            String base = sessionId.substring(0, index);
            newId = base + "." + jvmRoute;
         }
         
         // Fix the session's id
         resetSessionId(sessionId, newId);         
      }
      
      // Now we know the session object has a correct id
      // Also need to ensure any session cookie is correct
      if (setCookie)
      {
         // Check if the jvmRoute of the requested session id matches ours.
         // Only bother if we haven't already spotted a mismatch above
         if (newId == null)
         {
            String requestedJvmRoute = null;
            if (requestedId != null)
            {
               int reqIndex = requestedId.indexOf('.', 0);
               if (reqIndex > -1 && reqIndex < requestedId.length() - 1)
               {
                  requestedJvmRoute = requestedId.substring(reqIndex + 1, requestedId.length());
               }
            }
            
            if (!jvmRoute.equals(requestedJvmRoute))
            {
               if (log_.isTraceEnabled())
               {
                  log_.trace("handleJvmRoute(): We have detected a failover with different jvmRoute." +
                     " received one: " + requestedJvmRoute + " new one: " + jvmRoute + ". Will resent the session id.");
               }
               String base = index > -1 ? sessionId.substring(0, index) : sessionId;
               newId = new StringBuilder(base).append('.').append(jvmRoute).toString();
            }
         }

         /* Change the sessionid cookie if needed */
         if (newId != null)
            manager_.setNewSessionCookie(newId, response);
      }
   }
   
   /** 
    * Update the id of the given session
    * 
    *  @param oldId id of the session to change
    *  @param newId new session id the session object should have
    */
   private void resetSessionId(String oldId, String newId)
         throws IOException
   {
      @SuppressWarnings("unchecked")
      ClusteredSession<? extends OutgoingDistributableSessionData> session = (ClusteredSession) manager_.findSession(oldId);
      // change session id with the new one using local jvmRoute.
      if( session != null )
      {
         // Note this will trigger a session remove from the super Tomcat class.
         session.resetIdWithRouteInfo(newId);
         if (log_.isTraceEnabled())
         {
            log_.trace("resetSessionId(): changed catalina session to= [" + newId + "] old one= [" + oldId + "]");
         }
      }
      else if (log_.isTraceEnabled())
      {
         log_.trace("resetSessionId(): no session with id " + newId + " found");
      }
   }

   // Lifecycle Interface
   public void addLifecycleListener(LifecycleListener listener)
   {
      support.addLifecycleListener(listener);
   }

   public void removeLifecycleListener(LifecycleListener listener)
   {
      support.removeLifecycleListener(listener);
   }

   public LifecycleListener[] findLifecycleListeners()
   {
      return support.findLifecycleListeners();
   }

   public void start() throws LifecycleException
   {
      support.fireLifecycleEvent(START_EVENT, this);
   }

   public void stop() throws LifecycleException
   {
      support.fireLifecycleEvent(STOP_EVENT, this);
   }

}
