/*
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.remoting;

import org.jboss.logging.Logger;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Map;

/**
 * Acts as a persistent list which writes Serializable objects to disk and will retrieve them
 * in same order in which they were added (FIFO).  Each file will be named according to the current
 * time (using System.currentTimeMillis() with the file suffix specified (see below).  When the
 * object is read and returned by calling the getNext() method, the file on disk for that object will
 * be deleted.  If for some reason the store VM crashes, the objects will still be available upon next startup.
 * <p/>
 * The attributes to make sure to configure are:
 * <p/>
 * file path - this determins which directory to write the objects.  The default value is the property value
 * of 'jboss.server.data.dir' and if this is not set, then will be 'data'.  For example, might
 * be /jboss/server/default/data.<p>
 * file suffix - the file suffix to use for the file written for each object stored.<p>
 * <p/>
 * This is also a service mbean, so can be run as a service within JBoss AS or stand alone.
 *
 * @author <a href="mailto:tom@jboss.org">Tom Elrod</a>
 */
public class CallbackStore implements CallbackStoreMBean
{
   private String filePath = null;
   private String fileSuffix = "ser";

   private boolean isStarted = false;
   private boolean purgeOnShutdown = false;

   public static final String FILE_PATH_KEY = "StoreFilePath";
   public static final String FILE_SUFFIX_KEY = "StoreFileSuffix";

   private static final Logger log = Logger.getLogger(CallbackStore.class);

   /**
    * Default store constructor.
    */
   public CallbackStore()
   {

   }

   /**
    * Store constructor.
    * @param purgeOnDestroy if true, will remove all persisted objects from disk on when destroy() is called, else
    * will leave the files (which is the default behaviour).
    */
   public CallbackStore(boolean purgeOnDestroy)
   {
      this.purgeOnShutdown = purgeOnDestroy;
   }

   /**
    * Will get the file path value (if not already set will just use the
    * default setting) and will create the directory specified by the file path
    * if it does not already exist.
    *
    * @throws Exception
    */
   public void start() throws Exception
   {
      if(!isStarted)
      {
         // need to figure the best place to store on disk
         if(filePath == null)
         {
            filePath = System.getProperty("jboss.server.data.dir", "data");
         }
         File storeFile = new File(filePath);
         if(!storeFile.exists())
         {
            boolean madeDir = storeFile.mkdirs();
            if(!madeDir)
            {
               throw new IOException("Can not create directory for store.  Path given: " + filePath);
            }
         }
         isStarted = true;
      }
   }

   /**
    * Sets if store should clean up persisted files when shutdown (destroy()).
    * @param purgeOnShutdown
    */
   public void setPurgeOnShutdown(boolean purgeOnShutdown)
   {
      this.purgeOnShutdown = purgeOnShutdown;
   }

   /**
    * Returns if store will clean up persisted files when shutdown (destroy()).
    * @return
    */
   public boolean getPurgeOnShutdown()
   {
      return purgeOnShutdown;
   }

   /**
    * This is a no op method, but needed in order to be used as a service within JBoss AS.
    *
    * @throws Exception
    */
   public void create() throws Exception
   {
   }

   /**
    * This will allow for change of file suffix and file path and then may start again
    * using these new values.  However, any object already written out using the old
    * values will be lost as will not longer be accessible if these attributes are changed while stopped.
    */
   public void stop()
   {
      isStarted = false;
   }

   /**
    * If purgeOnDestroy is true, will remove files upon shutdown.
    */
   public void destroy()
   {
      if(purgeOnShutdown)
      {
         purgeFiles();
      }
   }

   public void purgeFiles()
   {
      String[] fileList = getObjectFileList();
      String fileToDelete = null;
      for(int x = 0; x < fileList.length; x++)
      {
         try
         {
            fileToDelete = filePath + System.getProperty("file.separator") + fileList[x];
            File currentFile = new File(fileToDelete);
            boolean deleted = currentFile.delete();
            if(!deleted)
            {
               log.warn("Error purging file " + fileToDelete);
            }
         }
         catch(Exception e)
         {
            log.warn("Error purging file " + fileToDelete);
         }
      }
   }

   /**
    * Will use the values in the map to set configuration.  This will not change behaviour of store until
    * has been stopped and then started (if has not been started, will take effect upon start).
    * The keys for the map are FILE_PATH_KEY and FILE_SUFFIX_KEY.
    *
    * @param config
    */
   public void setConfig(Map config)
   {
      if(config != null)
      {
         String newFilePath = (String) config.get(FILE_PATH_KEY);
         if(newFilePath != null)
         {
            filePath = newFilePath;
         }
         String newFileSuffix = (String) config.get(FILE_SUFFIX_KEY);
         if(newFileSuffix != null)
         {
            fileSuffix = newFileSuffix;
         }
      }
   }

   /**
    * Gets the file path for the directory where the objects will be stored.
    *
    * @return
    */
   public String getStoreFilePath()
   {
      return filePath;
   }

   /**
    * Sets teh file path for the directory where the objects will be stored.
    *
    * @param filePath
    */
   public void setStoreFilePath(String filePath)
   {
      this.filePath = filePath;
   }

   /**
    * Gets the file suffix for each of the files that objects will be persisted to.
    *
    * @return
    */
   public String getStoreFileSuffix()
   {
      return fileSuffix;
   }

   /**
    * Sets the file suffix for each of the files that objects will be persisted to.
    *
    * @param fileSuffix
    */
   public void setStoreFileSuffix(String fileSuffix)
   {
      this.fileSuffix = fileSuffix;
   }


   /**
    * Getst the number of objects stored and available.
    *
    * @return
    */
   public int size()
   {
      verifyStarted();
      String[] objectFileList = getObjectFileList();
      if(objectFileList != null)
      {
         return objectFileList.length;
      }
      else
      {
         return 0;
      }
   }

   private void verifyStarted()
   {
      if(!isStarted)
      {
         throw new RuntimeException("Can not call upon this store method before it has been started.");
      }
   }

   /**
    * Will look through the files in the store directory for the oldest object serialized to disk, load it,
    * delete the file, and return the deserialized object.
    * Important to note that once this object is returned from this method, it is gone forever from this
    * store and will not be able to retrieve it again without adding it back.
    *
    * @return
    * @throws IOException
    */
   public Object getNext() throws IOException
   {
      verifyStarted();

      Object obj = null;
      String objectFilePath = null;

      synchronized(filePath)
      {
         String[] objectFileList = getObjectFileList();
         FileInputStream inFile = null;
         ObjectInputStream in = null;

         if(objectFileList != null && objectFileList.length > 0)
         {
            try
            {
               // only getting the first one, which will be first one entered since the getting
               // of the list is automatically ordered by the OS and all file names are numeric by time.
               objectFilePath = filePath + System.getProperty("file.separator") + objectFileList[0];
               inFile = new FileInputStream(objectFilePath);
               in = new ObjectInputStream(inFile);
               try
               {
                  obj = in.readObject();
               }
               catch(ClassNotFoundException e)
               {
                  throw new IOException("Error loading persisted object.  Could not load class (" + e.getMessage() + ").");
               }
            }
            finally
            {
               if(inFile != null)
               {
                  try
                  {
                     inFile.close();
                  }
                  catch(IOException ioe)
                  {
                     log.debug("Error closing FileInputStream.", ioe);
                  }
               }
               if(in != null)
               {
                  try
                  {
                     in.close();
                  }
                  catch(IOException ioe)
                  {
                     log.debug("Error closing ObjectInputStream.", ioe);
                  }
               }
            }
         }
      }

      if(objectFilePath != null)
      {
         // now remove the file
         File objectFile = new File(objectFilePath);
         boolean isDeleted = objectFile.delete();
         if(log.isTraceEnabled())
         {
            log.trace("object file (" + objectFilePath + ") has been deleted - " + isDeleted);
         }
      }

      return obj;
   }

   private String[] getObjectFileList()
   {
      File storePath = new File(filePath);
      String[] objectFileList = storePath.list(new StoreFileFilter());
      return objectFileList;
   }

   /**
    * Persists the serializable object passed to the directory specified.  The file name will be the current time
    * in milliseconds (vis System.currentTimeMillis()) with the specified suffix.  This object can later be
    * retrieved using the getNext() method, but objects will be returned in the order that they were added (FIFO).
    *
    * @param object
    * @throws IOException
    */
   public void add(Serializable object) throws IOException
   {
      verifyStarted();

      synchronized(filePath)
      {
         long currentTimestamp = System.currentTimeMillis();
         File storeFile = new File(filePath + System.getProperty("file.separator") + String.valueOf(currentTimestamp) + "." + fileSuffix);
         FileOutputStream outFile = null;
         ObjectOutputStream out = null;

         try
         {
            outFile = new FileOutputStream(storeFile, false);
            out = new ObjectOutputStream(outFile);
            out.writeObject(object);
            out.flush();
         }
         finally
         {
            if(outFile != null)
            {
               try
               {
                  outFile.close();
               }
               catch(IOException ioe)
               {
                  log.debug("Error closing FileInputStream.", ioe);
               }
            }
            if(out != null)
            {
               try
               {
                  out.close();
               }
               catch(IOException ioe)
               {
                  log.debug("Error closing ObjectInputStream.", ioe);
               }
            }

         }
      }
   }

   public class StoreFileFilter implements FilenameFilter
   {
      /**
       * Tests if a specified file should be included in a file list.
       *
       * @param dir  the directory in which the file was found.
       * @param name the name of the file.
       * @return <code>true</code> if and only if the name should be included in the file list; <code>false</code>
       *         otherwise.
       */
      public boolean accept(File dir, String name)
      {
         if(name.endsWith(fileSuffix))
         {
            return true;
         }
         else
         {
            return false;
         }
      }
   }

}