content: /*
   * CDDL HEADER START
   *
   * The contents of this file are subject to the terms of the Common Development
   * and Distribution License (the "License").
   * You may not use this file except in compliance with the License.
   *
   * You can obtain a copy of the license at
   * src/com/vodafone360/people/VODAFONE.LICENSE.txt or
   * http://github.com/360/360-Engine-for-Android
   * See the License for the specific language governing permissions and
   * limitations under the License.
   *
   * When distributing Covered Code, include this CDDL HEADER in each file and
   * include the License file at src/com/vodafone360/people/VODAFONE.LICENSE.txt.
   * If applicable, add the following below this CDDL HEADER, with the fields
   * enclosed by brackets "[]" replaced with your own identifying information:
   * Portions Copyright [yyyy] [name of copyright owner]
   *
   * CDDL HEADER END
   *
   * Copyright 2010 Vodafone Sales & Services Ltd.  All rights reserved.
   * Use is subject to license terms.
   */
  
  package com.vodafone360.people.service.utils;
  
  import java.util.ArrayList;
  import java.util.LinkedList;
  import java.util.List;
  
  import com.vodafone360.people.datatypes.BaseDataType;
  import com.vodafone360.people.datatypes.ServerError;
  import com.vodafone360.people.service.io.QueueManager;
  import com.vodafone360.people.service.io.Request;
  import com.vodafone360.people.service.io.ResponseQueue.DecodedResponse;
  import com.vodafone360.people.utils.LogUtils;
  
  /**
   * TimeOutWatcher utility... This is a utility class that is intended to
   * dispatch time-outs for each sent request individually. The thread should be
   * managing only the requests which can expire, i.e. don't add request with
   * timeout "-1" TODO: Consider using the WorkerThread to check for timeouts,
   * that would save us one thread! !!! Also ResponseQueue, RequestQueue and
   * TimeOutWatcher should be managed together and used via a common mutex !!!
   */
  public class TimeOutWatcher implements Runnable {
  
      /**
       * Flag to determine whether or not the watcher is running.
       */
      private boolean mIsRunning = false;
  
      /**
       * The list of watched requests sorted by their expiry dates in ascending
       * order.
       */
      private LinkedList<Request> mRequests;
  
      /**
       * The thread watching for timed out requests.
       */
      private Thread mThread;
  
      /**
       * Constructor.
       */
      public TimeOutWatcher() {
          LogUtils.logV("TimeOutWatcher.TimeOutWatcher() => TimeOutWatcher constructor called.");
      }
  
      /**
       * TimeOutWatcher thread that checks for the next time to run, sleeps when
       * not busy and sends timeouts.
       */
      @Override
      public void run() {
  
          long nextRuntime, currentTime;
  
          // need to synchronize it all but calling wait() will release the lock
          synchronized (QueueManager.getInstance().lock) {
              while (mIsRunning) {
                  nextRuntime = getNextRuntime();
                  LogUtils.logV("TimeOutWatcher.run(): nextRuntime=" + nextRuntime);
                  if (nextRuntime < 0) {
                      // nothing to watch at the moment, let's wait for a
                      // notification for "synchronized(this)"
                      try {
                          LogUtils.logV("TimeOutWatcher.run(): nothing to watch, calling wait()");
                          QueueManager.getInstance().lock.wait();
                      } catch (InterruptedException e) {
                          LogUtils.logW("TimeOutWatcher.run(): "
                                  + "InterruptedException whithin this.wait() => " + e);
                      }
                  } else {
                      // one or more requests need to be watched
                      currentTime = System.currentTimeMillis();
                      nextRuntime = nextRuntime - currentTime;
                      if (nextRuntime > 0) {
                          // no request timed out yet, let's wait for the next
                          // timeout
                          try {
                              LogUtils.logV("TimeOutWatcher.run(): no time out yet, calling wait("
                                      + nextRuntime + ")");
                              QueueManager.getInstance().lock.wait(nextRuntime);
                          } catch (InterruptedException e) {
                              LogUtils.logW("TimeOutWatcher.run(): "
                                      + "InterruptedException within this.wait(nextRuntime) => " + e);
                          }
                      } else {
                          // one or more request have timed out, send them a time
                          // out event
                          sendTimeoutEvent(currentTime);
                      }
                  }
              }
              mThread = null;
          }
      }
  
      /**
       * Starts the TimeOutWatcher thread and performs initialization. Note: this
       * method shall be called within synchronized(this) block.
       */
      private void startThread() {
          mIsRunning = true;
          mRequests = new LinkedList<Request>();
          mThread = new Thread(this);
          mThread.start();
      }
  
      /**
       * Stops the TimeOutWatcher thread and releases the memory. Note: this
       * method shall be called within synchronized(this) block.
       */
      private void stopThread() {
  
          if (mIsRunning) {
              mRequests.clear();
              mRequests = null;
              // let the thread die
              mIsRunning = false;
              QueueManager.getInstance().lock.notify();
          }
      }
  
      /**
       * Finds the closest time to perform a new check on timeouts.
       * 
       * @return the next time when a timeout check is needed, -1 if no nothing to
       *         perform
       */
      private long getNextRuntime() {
          // just get the first one as the list of requests is sorted
          final Request request = mRequests.peek();
  
          if (request == null) {
              return -1;
          }
          return request.getExpiryDate();
      }
  
      /**
       * Sends a timeout event for all the expired requests.
       * 
       * @param currentTime the current time until when a timeout event needs to
       *            be sent
       */
      private void sendTimeoutEvent(long currentTime) {
          LogUtils.logV("TimeOutWatcher.sendTimeoutEvent(" + currentTime + ")");
  
          while (mRequests.size() > 0) {
              final Request request = mRequests.get(0);
              if (request.getExpiryDate() <= currentTime) {
                  LogUtils.logW("TimeOutWatcher.sendTimeoutEvent(): "
                          + "Expired request found with reqId=[" + request.getRequestId()
                          + "], type=["+request.mType+"] and timeout=" + request.getTimeout() + " milliseconds");
                  fireRequestExpired(request);
                  // no need to remove the request, this happened during previous
                  // method call... (removeRequest is called when adding a
                  // response)
              } else {
                  // the list is ordered by expiry date, no need to check the rest
                  // of it
                  break;
              }
          }
      }
  
      /**
       * Inserts a request in the requests list while maintaining it sorted by
       * ascending order of expiry date.
       * 
       * @param request the request to insert
       */
      private int insertRequestByExpiryDate(Request request) {
          for (int i = 0; i < mRequests.size(); i++) {
              final Request currentRequest = mRequests.get(i);
              if (currentRequest.getExpiryDate() > request.getExpiryDate()) {
                  // add the request before the current one
                  mRequests.add(i, request);
                  return i;
              }
          }
          // the request is either the first one or the last one
          mRequests.add(request);
          if (mRequests.size() == 1) {
              // was the only one
              return 0;
          }
          // was added at the end
          return mRequests.size() - 1;
      }
  
      /**
       * Creates a TimeOut event and adds it to the response queue. FIXME: this
       * assumes that adding a request to the response queue will trigger a
       * synchronous removeRequest() call with the same thread.
       * 
       * @param request the request that has timed out
       */
      private void fireRequestExpired(Request request) {
          // create a list with a server error containing a timeout
          final List<BaseDataType> data = new ArrayList<BaseDataType>(1);
          final ServerError timeoutError = new ServerError(ServerError.ErrorType.REQUEST_TIMEOUT, request.getRequestId());
          timeoutError.errorDescription = "TimeOutWatcher detected that the request id=["
                  + request.getRequestId() + "] has timed out.";
          data.add(timeoutError);
          // set the request as expired
          request.expired = true;
          // add the timeout error to the response queue
          LogUtils.logW("TimeOutWatcher.fireRequestExpired(): "
                  + "adding a timeout error to the response queue for reqId=["
                  + request.getRequestId() + "]");
          QueueManager.getInstance().addResponse(
                  new DecodedResponse(request.getRequestId(), data, request.mEngineId, DecodedResponse.ResponseType.SERVER_ERROR.ordinal()));
      }
  
      /**
       * Adds a request to be watched for timeouts. Note: it is assumed that the
       * request expiry date is calculated.
       * 
       * @param request the request to add
       */
      public void addRequest(Request request) {
          synchronized (QueueManager.getInstance().lock) {
              // make sure to add requests with a valid timeout
              if ((request != null) && (request.getExpiryDate() >= 0)) {
                  if (!mIsRunning) {
                      // start the thread if not already existing
                      startThread();
                  }
                  // insert the request in the sorted requests list
                  final int index = insertRequestByExpiryDate(request);
                  // check if request is added in front of the others. If not, no
                  // need to notify, we can still sleep!
                  if (index == 0) {
                      LogUtils.logV("TimeOutWatcher.addRequest(): wake up the thread");
                      QueueManager.getInstance().lock.notify();
                  }
              }
          }
      }
  
      /**
       * Removes a request from being watched for timeouts.
       * 
       * @param request the request to remove
       */
      public void removeRequest(Request request) {
  
          // The TimeOutWatcher is not initialized or has been stopped already,
          // just ignore the request
          if (!mIsRunning)
              return;
  
          synchronized (QueueManager.getInstance().lock) {
              if ((request != null) && (request.getExpiryDate() >= 0) && mRequests != null) {
                  int index = -1;
                  for (int i = 0; i < mRequests.size(); i++) {
                      final Request currentRequest = mRequests.get(i);
                      if (currentRequest == request) {
                          index = i;
                          mRequests.remove(i);
                          break;
                      }
                  }
                  // check if the first request was removed. If not, no need to
                  // notify, we can still sleep!
                  if (index == 0) {
                      LogUtils.logV("TimeOutWatcher.removeRequest(): wake up the thread");
                      QueueManager.getInstance().lock.notify();
                  }
              }
          }
      }
  
      /**
       * Kills the TimeOutWatcher (releases memory and running thread).
       */
      public void kill() {
          synchronized (QueueManager.getInstance().lock) {
              stopThread();
          }
      }
  
      /**
       * Sends a timeout event for all the requests.
       */
      public void invalidateAllRequests() {
          synchronized (QueueManager.getInstance().lock) {
              LogUtils.logV("TimeOutWatcher.invalidateAllRequests()");
  
              if (mRequests == null)
                  return;
  
              while (mRequests.size() > 0) {
                  final Request request = mRequests.get(0);
                  LogUtils.logV("TimeOutWatcher.invalidateAllRequests(): "
                          + "forcing a timeout for reqId=[" + request.getRequestId()
                          + "] and timeout=" + request.getTimeout() + " milliseconds");
                  fireRequestExpired(request);
                  // no need to remove the request, this happened during previous
                  // method call... (removeRequest is called when adding a
                  // response)
              }
          }
      }
  
      // ////////////////
      // TEST METHODS //
      // ////////////////
  
      // TODO: should have package access only but requires to modify the test
      // package to have the same package name
  
      /**
       * Gets the current number of requests being watched for timeouts.
       */
      public int getRequestsCount() {
          synchronized (QueueManager.getInstance().lock) {
              if (mRequests != null) {
                  return mRequests.size();
              }
  
              return 0;
          }
      }
  
      /**
       * Gets an array containing all the requests being watched for timeouts.
       * 
       * @return array containing all requests, NULL if list of requests
       *         maintained internally is NLL or empty.
       */
      public Request[] getRequestsArray() {
          synchronized (QueueManager.getInstance().lock) {
              if (mRequests != null && mRequests.size() > 0) {
                  final Request[] requests = new Request[mRequests.size()];
                  return mRequests.toArray(requests);
              }
  
              return null;
          }
      }
  }
  