/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2006, 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.txtimer;

// $Id: TimerImpl.java 88432 2009-05-08 13:37:05Z galder.zamarreno@jboss.com $

import java.io.Serializable;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

import javax.ejb.EJBException;
import javax.ejb.NoSuchObjectLocalException;
import javax.ejb.TimerHandle;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.Transaction;

import org.jboss.ejb.AllowedOperationsAssociation;
import org.jboss.logging.Logger;

/**
 * An implementation of an EJB Timer.
 * 
 * Internally it uses a java.util.Timer and maintains its state in
 * a Tx manner.
 *
 * @author Thomas.Diesler@jboss.org
 * @author Dimitris.Andreadis@jboss.org
 * @version $Revision: 88432 $
 * @since 07-Apr-2004
 */
public class TimerImpl implements javax.ejb.Timer, Synchronization
{
   // logging support
   private static Logger log = Logger.getLogger(TimerImpl.class);

   /**
    * Timer states and their allowed transitions
    * <p/>
    * CREATED  - on create
    * CREATED -> STARTED_IN_TX - when strated with Tx
    * CREATED -> ACTIVE  - when started without Tx
    * STARTED_IN_TX -> ACTIVE - on Tx commit
    * STARTED_IN_TX -> CANCELED - on Tx rollback
    * ACTIVE -> CANCELED_IN_TX - on cancel() with Tx
    * ACTIVE -> CANCELED - on cancel() without Tx
    * CANCELED_IN_TX -> CANCELED - on Tx commit
    * CANCELED_IN_TX -> ACTIVE - on Tx rollback
    * ACTIVE -> IN_TIMEOUT - on TimerTask run
    * IN_TIMEOUT -> ACTIVE - on Tx commit if periode > 0
    * IN_TIMEOUT -> EXPIRED -> on Tx commit if periode == 0
    * IN_TIMEOUT -> RETRY_TIMEOUT -> on Tx rollback
    * RETRY_TIMEOUT -> ACTIVE -> on Tx commit/rollback if periode > 0
    * RETRY_TIMEOUT -> EXPIRED -> on Tx commit/rollback if periode == 0
    */
   private static final int CREATED = 0;
   private static final int STARTED_IN_TX = 1;
   private static final int ACTIVE = 2;
   private static final int CANCELED_IN_TX = 3;
   private static final int CANCELED = 4;
   private static final int EXPIRED = 5;
   private static final int IN_TIMEOUT = 6;
   private static final int RETRY_TIMEOUT = 7;

   private static final String[] TIMER_STATES = {"created", "started_in_tx", "active", "canceled_in_tx",
                                                 "canceled", "expired", "in_timeout", "retry_timeout"};

   // The initial txtimer properties
   private TimerServiceImpl timerService;
   private String timerId;
   private TimedObjectId timedObjectId;
   private TimedObjectInvoker timedObjectInvoker;
   private Date firstTime;
   private long periode;
   private Serializable info;

   private long nextExpire;
   private int timerState;
   private Timer utilTimer;
   private int hashCode;

   /**
    * Schedules the txtimer for execution at the specified time with a specified periode.
    */
   TimerImpl(TimerServiceImpl timerService, String timerId, TimedObjectId timedObjectId, TimedObjectInvoker timedObjectInvoker, Serializable info)
   {
      this.timerService = timerService;
      this.timerId = timerId;
      this.timedObjectId = timedObjectId;
      this.timedObjectInvoker = timedObjectInvoker;
      this.info = info;

      setTimerState(CREATED);
   }

   void startTimer(Date firstTime, long periode)
   {
      this.firstTime = firstTime;
      this.nextExpire = firstTime.getTime();
      this.periode = periode;

      timerService.addTimer(this);
      registerTimerWithTx();
      
      // the timer will actually go ACTIVE on tx commit
      startInTx();
   }

   public String getTimerId()
   {
      return timerId;
   }

   public TimedObjectId getTimedObjectId()
   {
      return timedObjectId;
   }

   public Date getFirstTime()
   {
      return firstTime;
   }

   public long getPeriode()
   {
      return periode;
   }

   public long getNextExpire()
   {
      return nextExpire;
   }

   public Serializable getInfoInternal()
   {
      return info;
   }

   public boolean isActive()
   {
      return !isCanceled() && !isExpired();
   }

   public boolean isInRetry() {
      return timerState == RETRY_TIMEOUT;
   }

   public boolean isCanceled()
   {
      return timerState == CANCELED_IN_TX || timerState == CANCELED;
   }

   public boolean isExpired()
   {
      return timerState == EXPIRED;
   }

   /**
    * Cause the txtimer and all its associated expiration notifications to be cancelled.
    *
    * @throws IllegalStateException  If this method is invoked while the instance is in
    *                                a state that does not allow access to this method.
    * @throws javax.ejb.NoSuchObjectLocalException
    *                                If invoked on a txtimer that has expired or has been cancelled.
    * @throws javax.ejb.EJBException If this method could not complete due to a system-level failure.
    */
   public void cancel() throws IllegalStateException, NoSuchObjectLocalException, EJBException
   {
      assertTimedOut();
      assertAllowedOperation("Timer.cancel");
      registerTimerWithTx();
      cancelInTx();
   }

   /**
    * Kill the timer, and remove it from the timer service
    */
   public void killTimer()
   {
      log.debug("killTimer: " + this);
      if (timerState != EXPIRED)
         setTimerState(CANCELED);
      timerService.removeTimer(this);
      utilTimer.cancel();
   }

   /**
    * killTimer w/o persistence work
    */
   private void cancelTimer()
   {
      if (timerState != EXPIRED)
         setTimerState(CANCELED);
      utilTimer.cancel();
   }

   /**
    * Kill the timer, do not remove from timer service
    */
   public void stopTimer()
   {
      log.debug("stopTimer: " + this);
      if (timerState != EXPIRED)
         setTimerState(CANCELED);
      utilTimer.cancel();      
   }
   
   /**
    * Get the number of milliseconds that will elapse before the next scheduled txtimer expiration.
    *
    * @return Number of milliseconds that will elapse before the next scheduled txtimer expiration.
    * @throws IllegalStateException  If this method is invoked while the instance is in
    *                                a state that does not allow access to this method.
    * @throws javax.ejb.NoSuchObjectLocalException
    *                                If invoked on a txtimer that has expired or has been cancelled.
    * @throws javax.ejb.EJBException If this method could not complete due to a system-level failure.
    */
   public long getTimeRemaining() throws IllegalStateException, NoSuchObjectLocalException, EJBException
   {
      assertTimedOut();
      assertAllowedOperation("Timer.getTimeRemaining");
      return nextExpire - System.currentTimeMillis();
   }

   /**
    * Get the point in time at which the next txtimer expiration is scheduled to occur.
    *
    * @return Get the point in time at which the next txtimer expiration is scheduled to occur.
    * @throws IllegalStateException  If this method is invoked while the instance is in
    *                                a state that does not allow access to this method.
    * @throws javax.ejb.NoSuchObjectLocalException
    *                                If invoked on a txtimer that has expired or has been cancelled.
    * @throws javax.ejb.EJBException If this method could not complete due to a system-level failure.
    */
   public Date getNextTimeout() throws IllegalStateException, NoSuchObjectLocalException, EJBException
   {
      assertTimedOut();
      assertAllowedOperation("Timer.getNextTimeout");
      return new Date(nextExpire);
   }

   /**
    * Get the information associated with the txtimer at the time of creation.
    *
    * @return The Serializable object that was passed in at txtimer creation, or null if the
    *         info argument passed in at txtimer creation was null.
    * @throws IllegalStateException  If this method is invoked while the instance is in
    *                                a state that does not allow access to this method.
    * @throws javax.ejb.NoSuchObjectLocalException
    *                                If invoked on a txtimer that has expired or has been cancelled.
    * @throws javax.ejb.EJBException If this method could not complete due to a system-level failure.
    */
   public Serializable getInfo() throws IllegalStateException, NoSuchObjectLocalException, EJBException
   {
      assertTimedOut();
      assertAllowedOperation("Timer.getInfo");
      return info;
   }

   /**
    * Get a serializable handle to the txtimer. This handle can be used at a later time to
    * re-obtain the txtimer reference.
    *
    * @return Handle of the Timer
    * @throws IllegalStateException  If this method is invoked while the instance is in
    *                                a state that does not allow access to this method.
    * @throws javax.ejb.NoSuchObjectLocalException
    *                                If invoked on a txtimer that has expired or has been cancelled.
    * @throws javax.ejb.EJBException If this method could not complete due to a system-level failure.
    */
   public TimerHandle getHandle() throws IllegalStateException, NoSuchObjectLocalException, EJBException
   {
      assertTimedOut();
      assertAllowedOperation("Timer.getHandle");
      return new TimerHandleImpl(this);
   }

   /**
    * Return true if objectId, createDate, periode are equal
    */
   public boolean equals(Object obj)
   {
      if (obj == this) return true;
      if (obj instanceof TimerImpl)
      {
         TimerImpl other = (TimerImpl)obj;
         return hashCode() == other.hashCode();
      }
      return false;
   }

   /**
    * Hash code based on the Timers invariant properties
    */
   public int hashCode()
   {
      if (hashCode == 0)
      {
         String hash = "[" + timerId + "," + timedObjectId + "," + firstTime + "," + periode + "]";
         hashCode = hash.hashCode();
      }
      return hashCode;
   }

   /**
    * Returns a string representation of the object.
    */
   public String toString()
   {
      long remaining = nextExpire - System.currentTimeMillis();
      String retStr = "[id=" + timerId + ",target=" + timedObjectId + ",remaining=" + remaining + ",periode=" + periode +
              "," + TIMER_STATES[timerState] + "]";
      return retStr;
   }

   /**
    * Register the txtimer with the current transaction
    */
   private void registerTimerWithTx()
   {
      Transaction tx = timerService.getTransaction();
      if (tx != null)
      {
         try
         {
            tx.registerSynchronization(this);
         }
         catch (Exception e)
         {
            log.error("Cannot register txtimer with Tx: " + this);
         }
      }
   }

   private void setTimerState(int state)
   {
      log.debug("setTimerState: " + TIMER_STATES[state]);
      timerState = state;
   }

   private void startInTx()
   {
      // JBAS-4330, provide a meaningful name to the timer thread, needs jdk5+
      utilTimer = new Timer("EJB-Timer-" + timerId + timedObjectId);
      
      if (timerService.getTransaction() != null)
      {
         // don't schedule the timeout yet
         setTimerState(STARTED_IN_TX);
      }
      else
      {
         setTimerState(ACTIVE);
         scheduleTimeout();
      }
   }

   private void cancelInTx()
   {
      if (timerService.getTransaction() != null)
         setTimerState(CANCELED_IN_TX);
      else
         killTimer();
   }

   private void scheduleTimeout()
   {
      if (periode > 0)
         utilTimer.schedule(new TimerTaskImpl(this), new Date(nextExpire), periode);
      else
         utilTimer.schedule(new TimerTaskImpl(this), new Date(nextExpire));      
   }
   
   /**
    * Throws NoSuchObjectLocalException if the txtimer was canceled or has expired
    */
   private void assertTimedOut()
   {
      if (timerState == EXPIRED)
         throw new NoSuchObjectLocalException("Timer has expired");
      if (timerState == CANCELED_IN_TX || timerState == CANCELED)
         throw new NoSuchObjectLocalException("Timer was canceled");
   }

   /**
    * Throws an IllegalStateException if the Timer method call is not allowed in the current context
    */
   private void assertAllowedOperation(String timerMethod)
   {
      AllowedOperationsAssociation.assertAllowedIn(timerMethod,
              AllowedOperationsAssociation.IN_BUSINESS_METHOD |
              AllowedOperationsAssociation.IN_EJB_TIMEOUT |
              AllowedOperationsAssociation.IN_SERVICE_ENDPOINT_METHOD |
              AllowedOperationsAssociation.IN_AFTER_BEGIN |
              AllowedOperationsAssociation.IN_BEFORE_COMPLETION |
              AllowedOperationsAssociation.IN_EJB_POST_CREATE |
              AllowedOperationsAssociation.IN_EJB_REMOVE |
              AllowedOperationsAssociation.IN_EJB_LOAD |
              AllowedOperationsAssociation.IN_EJB_STORE);
   }

   // Synchronization **************************************************************************************************

   /**
    * This method is invoked before the start of the commit or rollback
    * process. The method invocation is done in the context of the
    * transaction that is about to be committed or rolled back.
    */
   public void beforeCompletion()
   {
      switch(timerState)
      {
         case CANCELED_IN_TX:
            timerService.removeTimer(this);
            break;

         case IN_TIMEOUT:
         case RETRY_TIMEOUT:
            if(periode == 0)
            {
               timerService.removeTimer(this);
            }
            break;
      }
   }

   /**
    * This method is invoked after the transaction has committed or
    * rolled back.
    *
    * @param status The status of the completed transaction.
    */
   public void afterCompletion(int status)
   {
      if (status == Status.STATUS_COMMITTED)
      {
         log.debug("commit: " + this);

         switch (timerState)
         {
            case STARTED_IN_TX:
               scheduleTimeout();
               setTimerState(ACTIVE);
               break;

            case CANCELED_IN_TX:
               cancelTimer();
               break;

            case IN_TIMEOUT:
            case RETRY_TIMEOUT:
               if(periode == 0)
               {
                  setTimerState(EXPIRED);
                  cancelTimer();
               }
               else
               {
                  setTimerState(ACTIVE);
               }
               break;
         }
      }
      else if (status == Status.STATUS_ROLLEDBACK)
      {
         log.debug("rollback: " + this);

         switch (timerState)
         {
            case STARTED_IN_TX:
               cancelTimer();
               break;
               
            case CANCELED_IN_TX:
               setTimerState(ACTIVE);
               break;
               
            case IN_TIMEOUT:
               setTimerState(RETRY_TIMEOUT);
               log.debug("retry: " + this);
               timerService.retryTimeout(this);
               break;
               
            case RETRY_TIMEOUT:
               if (periode == 0)
               {
                  setTimerState(EXPIRED);
                  cancelTimer();
               }
               else
               {
                  setTimerState(ACTIVE);
               }
               break;
         }
      }
   }
      
   // TimerTask ********************************************************************************************************

   /**
    * The TimerTask's run method is invoked by the java.util.Timer
    */
   private class TimerTaskImpl extends TimerTask
   {
      private TimerImpl timer;

      public TimerTaskImpl(TimerImpl timer)
      {
         this.timer = timer;
      }

      /**
       * The action to be performed by this txtimer task.
       */
      public void run()
      {
         log.debug("run: " + timer);

         // Set next scheduled execution attempt. This is used only
         // for reporting (getTimeRemaining()/getNextTimeout())
         // and not from the underlying jdk timer implementation.
         if (isActive() && periode > 0)
         {
            nextExpire += periode;
         }
         
         // If a retry thread is in progress, we don't want to allow another
         // interval to execute until the retry is complete. See JIRA-1926.
         if (isInRetry())
         {
            log.debug("Timer in retry mode, skipping this scheduled execution");
            return;
         }
         
         if (isActive())
         {
            try
            {
               setTimerState(IN_TIMEOUT);
               timedObjectInvoker.callTimeout(timer);
            }
            catch (Exception e)
            {
               log.error("Error invoking ejbTimeout", e);
            }
            finally
            {
               if (timerState == IN_TIMEOUT)
               {
                  log.debug("Timer was not registered with Tx, resetting state: " + timer);
                  if (periode == 0)
                  {
                     setTimerState(EXPIRED);
                     killTimer();
                  }
                  else
                  {
                     setTimerState(ACTIVE);
                  }
               }
            }
         }
      }
   }
}
