/*
 * 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.ejb.plugins;

import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.jboss.ejb.Container;
import org.jboss.ejb.EjbModule;
import org.jboss.ha.framework.interfaces.GenericClusteringException;
import org.jboss.invocation.Invocation;
import org.jboss.system.ServiceMBean;

import javax.management.AttributeChangeNotification;
import javax.management.AttributeChangeNotificationFilter;
import javax.management.Notification;
import javax.management.NotificationListener;

/**
 * Track the incoming invocations and when shuting down a container (stop or
 * destroy), waits for current invocations to finish before returning the
 * stop or destroy call. This interceptor can be important in clustered environment
 * where shuting down a node doesn't necessarly mean that an application cannot
 * be reached: other nodes may still be servicing. Consequently, it is important
 * to have a clean shutdown to keep a coherent behaviour cluster-wide.
 *
 * To avoid strange or inefficient behaviour, the facade session bean (if any)
 * should be stopped first thus not blocking invocations in a middle-step (i.e.
 * facade making multiple invocations to "sub-beans": if a "sub-bean" is
 * shut down, the facade will get an exception in the middle of its activity)
 *
 * @see <related>
 *
 * @author  <a href="mailto:sacha.labourey@cogito-info.ch">Sacha Labourey</a>.
 * @version $Revision: 89150 $
 *
 * <p><b>Revisions:</b>
 *
 * <p><b>14 avril 2002 Sacha Labourey:</b>
 * <ul>
 * <li> First implementation </li>
 * </ul>
 */

public class CleanShutdownInterceptor extends AbstractInterceptor
{

   // Constants -----------------------------------------------------

   // Attributes ----------------------------------------------------

   protected Container container = null;

   protected EjbModule ejbModule = null;
   protected String ejbModuleName = null;

   private static ThreadLocal<String> currentModule = new ThreadLocal<String>();

   protected boolean allowInvocations = false;
   protected boolean allowRemoteInvocations = false;

   protected boolean isDebugEnabled = false;

   public long runningInvocations = 0;
   public long runningHomeInvocations = 0;
   public long shutdownTimeout = 60000;
   public long readAcquireTimeMs = 10000;

   protected ReadWriteLock rwLock = new ReentrantReadWriteLock(true);

   // Static --------------------------------------------------------

   private static final String METHOD_INVOCATION_TAG = "WrappingEjbModuleName";

   // Constructors --------------------------------------------------

   public CleanShutdownInterceptor ()
   {
   }

   // Public --------------------------------------------------------

   public void onlyAllowLocalInvocations ()
   {
      if (isDebugEnabled) log.debug ("Only allow local invocation from now on: " + this.container.getServiceName ().toString ());
      this.allowRemoteInvocations = false;
   }

   public void waitForNoMoreInvocations ()
   {
      this.log.debug ("Waiting that the container " + container.getJmxName () + " finishes its running invocations. " +
                        this.runningHomeInvocations + " current home invocations and " +
                        this.runningInvocations + " current remote invocations.");

      purgeRunningInvocations ();
      if (isDebugEnabled) log.debug ("... Done: no more remote invocations currently running in this container.");
   }

   // Z implementation ----------------------------------------------

   // AbstractInterceptor overrides ---------------------------------------------------

   public void create() throws Exception {
      super.create ();
      this.allowInvocations = false;
      this.allowRemoteInvocations = false;

      this.isDebugEnabled = log.isDebugEnabled ();

      ejbModuleName = ejbModule.getServiceName().toString();

      // we register our inner-class to retrieve STATE notifications from our container
      //
      AttributeChangeNotificationFilter filter = new AttributeChangeNotificationFilter ();
      filter.enableAttribute ("State");

      this.container.getServer ().
         addNotificationListener (this.container.getEjbModule ().getServiceName (),
                                  new CleanShutdownInterceptor.StateChangeListener (),
                                  filter,
                                  null);

      // we need a way to find all CleanShutDownInterceptor of an EjbModule
      //
      ejbModule.putModuleData ("CleanShutDownInterceptor-" + this.container.getServiceName ().toString (), this);
   }

   public void start() throws Exception {
      super.start();
      this.allowInvocations = true;
      this.allowRemoteInvocations = true;
   }

   public void stop() {
      super.stop ();

      this.log.debug ("Stopping container " + container.getJmxName () + ". " +
                        this.runningHomeInvocations + " current home invocations and " +
                        this.runningInvocations + " current remote invocations.");

      forbidInvocations ();
   }

   public void destroy() {
      super.destroy ();

      this.log.debug ("Destroying container " + container.getJmxName ().toString () + ". " +
                        this.runningHomeInvocations + " current home invocations and " +
                        this.runningInvocations + " current remote invocations.");

      forbidInvocations() ;
   }

   public Object invokeHome (Invocation mi)
   throws Exception
   {
      if (this.allowInvocations)
      {
         String origin = getOrigin (mi);
         boolean isAppLocalCall = ejbModuleName.equals (origin);

         if (!this.allowRemoteInvocations && !isAppLocalCall)
            // it is a remote call and they are currently forbidden!
            //
         {
            if (isDebugEnabled) log.debug ("Refusing a remote home invocation. here= " + ejbModuleName + "; Origin= " + origin);
            throw new GenericClusteringException (GenericClusteringException.COMPLETED_NO,
                                                  "This application does not accept remote calls any more");
         }

         // we need to acquire the read lock. If we cannot directly, it means
         // that the stop/destroy call has gotten the write lock in the meantime
         //
         try
         {
            if (!isAppLocalCall) // we only consider remote calls => every local originates from a remote!
            {
               if (!rwLock.readLock().tryLock(readAcquireTimeMs, TimeUnit.MILLISECONDS))
                  throw new GenericClusteringException (GenericClusteringException.COMPLETED_NO,
                                                        "Container is shuting down on this node (timeout)");
            }
         }
         catch (java.lang.InterruptedException ie)
         {
            Thread.currentThread().interrupt();
            throw new GenericClusteringException (GenericClusteringException.COMPLETED_NO,
                                                  "Container is shuting down on this node");
         }

         runningHomeInvocations++;
         try
         {
            if (!isAppLocalCall)
               setOrigin (mi);
            return this.getNext ().invokeHome (mi);
         }
         catch (GenericClusteringException gce)
         {
            // a gce exception has be thrown somewhere else: we need to modify its flag
            // and forward it. We could add optimisations at this level by having some
            // "idempotent" flag at the container level
            //
            gce.setCompletionStatus (gce.COMPLETED_MAYBE);
            throw gce;
         }
         finally
         {
            if (!isAppLocalCall)
               revertOrigin (mi, origin);

            runningHomeInvocations--;
            if (!isAppLocalCall) // we only consider remote calls => every local originates from a remote!
               rwLock.readLock().unlock();
         }
      }
      else
         throw new GenericClusteringException (GenericClusteringException.COMPLETED_NO,
                                               "Container is not allowing invocations as it failed to start or is shutting down");
   }

   public Object invoke (Invocation mi)
   throws Exception
   {
      if (this.allowInvocations)
      {
         String origin = getOrigin (mi);
         boolean isAppLocalCall = ejbModuleName.equals (origin);

         if (!this.allowRemoteInvocations && !isAppLocalCall)
            // it is a remote call and they are currently forbidden!
            //
         {
            if (isDebugEnabled) log.debug ("Refusing a remote invocation");
            throw new GenericClusteringException (GenericClusteringException.COMPLETED_NO,
                                                  "This application does not accept remote calls any more");
         }

         // we need to acquire the read lock. If we cannot directly, it means
         // that the stop/destroy call has gotten the write lock in the meantime
         //
         try
         {
            if (!isAppLocalCall) // we only consider remote calls => every local originates from a remote!
            {
               if (!rwLock.readLock ().tryLock(readAcquireTimeMs, TimeUnit.MILLISECONDS))
                  throw new GenericClusteringException (GenericClusteringException.COMPLETED_NO,
                                                        "Container is shuting down on this node (timeout)");
            }
         }
         catch (java.lang.InterruptedException ie)
         {
            Thread.currentThread().interrupt();
            throw new GenericClusteringException (GenericClusteringException.COMPLETED_NO,
                                                  "Container is shuting down on this node");
         }

         runningInvocations++;
         try
         {
            if (!isAppLocalCall)
               setOrigin (mi);
            return this.getNext ().invoke (mi);
         }
         catch (GenericClusteringException gce)
         {
            // a gce exception has be thrown somewhere else: we need to modify its flag
            // and forward it. We could add optimisations at this level by having some
            // "idempotent" flag at the container level
            //
            gce.setCompletionStatus (gce.COMPLETED_MAYBE);
            throw gce;
         }
         finally
         {
            if (!isAppLocalCall)
               revertOrigin (mi, origin);

            runningInvocations--;
            if (!isAppLocalCall) // we only consider remote calls => every local originates from a remote!
               rwLock.readLock().unlock();
         }
      }
      else
         throw new GenericClusteringException (GenericClusteringException.COMPLETED_NO,
                                               "Container is not allowing invocations as it failed to start or is shutting down");
   }

   public Container getContainer ()
   {
      return this.container;
   }

   /** This callback is set by the container so that the plugin may access it
    *
    * @param con    The container using this plugin.
    */
   public void setContainer (Container con)
   {
      this.container = con;
      if (con != null)
         this.ejbModule = con.getEjbModule ();
      else
         this.ejbModule = null;
   }

   // Package protected ---------------------------------------------

   // Protected -----------------------------------------------------

   protected void forbidInvocations ()
   {
      this.allowInvocations = false;

      purgeRunningInvocations();
   }

   protected void purgeRunningInvocations ()
   {
      try
      {
         if (this.rwLock.writeLock().tryLock(shutdownTimeout, TimeUnit.MILLISECONDS))
            this.rwLock.writeLock().unlock();
         else
            log.info ("Possible running invocations not terminated " +
               "while leaving the container. Home: " + runningHomeInvocations +
               ". Remote: " + runningInvocations + ".");
      }
      catch (Exception e)
      {
         log.info ("Exception while waiting for running invocations " +
            "to leave container. Home: " + runningHomeInvocations +
            ". Remote: " + runningInvocations + ".", e);
      }
      finally
      {

      }
   }

   protected String getOrigin (Invocation mi)
   {
      String value = currentModule.get();
      if (log.isTraceEnabled())
         log.trace ("GET_ORIGIN: " + value + " in " + this.container.getServiceName ().toString ());
      return value;
   }

   protected void setOrigin (Invocation mi)
   {
      currentModule.set(this.ejbModuleName);
   }

   protected void revertOrigin (Invocation mi, String origin)
   {

      if (log.isTraceEnabled()) log.trace ("Crossing ejbModule border from " + this.ejbModuleName + " to " + origin);
      currentModule.set (origin);
   }


   // Private -------------------------------------------------------

   protected void containerIsAboutToStop ()
   {
      // Distinction between between JMX proxies and underlying services seems
      // to lead to a 2nd STOPPING event; ignore it or we get an NPE
      if (ejbModule == null)
      {
         log.debug("Received STOPPING notification after container already set to null; ignoring");
         return;
      }

      log.debug ("Container about to stop: disabling HA-RMI access to bean from interceptor");

      // This is bad: we should have some kind of code (manager) associated
      // with this ejbModule. We mimic this by electing the first ProxyFactoryHA
      // as a manager
      //
      boolean iAmTheManager = !Boolean.TRUE.equals (ejbModule.getModuleData ("ShutdownInterceptorElected"));

      if (iAmTheManager)
      {
         ejbModule.putModuleData ("ShutdownInterceptorElected", Boolean.TRUE);

         if (isDebugEnabled) log.debug ("Container is about to stop and I am the manager of the first step: blocking remote calls");
         // in a first step, all interceptors must refuse/redirect remote invocations
         //
         Collection containers = ejbModule.getContainers ();
         Iterator containersIter = containers.iterator ();
         while (containersIter.hasNext ())
         {
            Container otherContainer = (Container)containersIter.next ();
            CleanShutdownInterceptor inter = (CleanShutdownInterceptor)
               ejbModule.getModuleData ("CleanShutDownInterceptor-" + otherContainer.getServiceName ().toString ());
            if (inter == null)
            {
               log.debug ("Found an EJB that doesnt have a clean-shutdown interceptor: " + otherContainer.getJmxName ());
            }
            else
            {
               inter.onlyAllowLocalInvocations ();
            }
         }
      }
      else
      {
         if (isDebugEnabled) log.debug ("Container is about to stop but I am not the manager: I don't manage the first step of the process.");
      }

      // in a second step, all container, manager or not, will wait that no more invocation
      // are running through
      // The cycling around other interceptor is managed by the JMX callbacks, not by us
      //
      waitForNoMoreInvocations ();
   }

   // Inner classes -------------------------------------------------

   class StateChangeListener implements NotificationListener
   {

      public void handleNotification (Notification notification, java.lang.Object handback)
      {
         if (notification instanceof AttributeChangeNotification)
         {
            AttributeChangeNotification notif = (AttributeChangeNotification) notification;
            int value = ((Integer)notif.getNewValue()).intValue ();

            // Start management is handled by the ProxyFactoryHA, not here
            if (value == ServiceMBean.STOPPING)
            {
               containerIsAboutToStop ();
            }
         }
      }

   }

   }
