/*
 * Copyright (C) 2009 eXo Platform SAS.
 *
 * 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.exoplatform.services.jcr.impl.util.io;


import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import org.exoplatform.services.jcr.impl.dataflow.SpoolConfig;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

/**
 * Created by The eXo Platform SAS Author : Peter Nedonosko peter.nedonosko@exoplatform.com.ua
 * 05.10.2007
 * 
 * For use in persistent layer, i.e. JDBCStorageConnection (ClenableFileValueData), will be shared
 * across the Workspace cache with many Sessions . Swap files creation (like in
 * JDBCStorageConnection.readValueData(String, int, int)) managed by SwapFile.get(File, String)
 * method. There are no way to get swap file in another way.
 * 
 * A SwapFile extends the SpoolFile. But in runtime all swap files will be stored in global map used
 * for prevent files rewriting in concurrent environment (case of issue JCR-329). Till the SwapFile
 * spool operation in progress all other users who will attempt to get the file will wait the
 * operation completion (java.util.concurrent.CountDownLatch used).
 * 
 * @author <a href="mailto:peter.nedonosko@exoplatform.com.ua">Peter Nedonosko</a>
 * @version $Id: SwapFile.java 11907 2008-03-13 15:36:21Z ksm $
 */
public class SwapFile extends SpoolFile
{

   /**
    * The serial version UID
    */
   private static final long serialVersionUID = 4048760909657109754L;

   /**
    * The Logger.
    */
   private static final Log LOG = ExoLogger.getLogger("exo.jcr.component.core.SwapFile");

   /**
    * In-share files database.
    */
   protected static final ConcurrentMap<String, WeakReference<SwapFile>> CURRENT_SWAP_FILES =
      new ConcurrentHashMap<String, WeakReference<SwapFile>>();

   /**
    * Spool latch.
    */
   protected final AtomicReference<CountDownLatch> spoolLatch = new AtomicReference<CountDownLatch>();

   /**
    * swap cleaner (FileCleaner).
    */
   private final FileCleaner swapCleaner;

   /**
    * SwapFile constructor.
    * 
    * @param parent
    *          Parent File
    * @param child
    *          File name
    * @param cleaner
    *          File Cleaner
    */
   protected SwapFile(File parent, String child, FileCleaner cleaner)
   {
      super(parent,child);
      this.swapCleaner=cleaner;
   }

   /**
    * Obtain SwapFile by parent file and name.
    *
    * @param parent
    *          - parent File
    * @param child
    *          - String with file name
    * @return SwapFile swap file
    * @throws IOException
    *           I/O error
    */
   public static SwapFile get(final File parent, final String child) throws IOException
   {

      return get(parent, child, SpoolConfig.getDefaultSpoolConfig().fileCleaner);
   }

   /**
    * Obtain SwapFile by parent file and name.
    * 
    * If the file was swapped before and still in use it will be returned, i.e. same object of
    * java.io.File will be returned.
    * 
    * If the file swapping (writing) now at this time the caller thread will wait till the swap
    * process will be finished.
    * 
    * @param parent
    *          - parent File
    * @param child
    *          - String with file name
    * @param cleaner
    *          - The FileCleaner
    * @return SwapFile swap file
    * @throws IOException
    *           I/O error
    */
   public static SwapFile get(final File parent, final String child, FileCleaner cleaner) throws IOException
   {
      SwapFile newsf = new SwapFile(parent, child,cleaner);
      String absPath = newsf.getAbsolutePath();

      WeakReference<SwapFile> swappedRef = CURRENT_SWAP_FILES.get(absPath);
      SwapFile swapped;
      if (swappedRef != null && (swapped = swappedRef.get()) != null)
      {
         // The swap file has been registered already
         do
         {
            // We loop until the spoolLatch is null
            CountDownLatch spoolLatch = swapped.spoolLatch.get();
            if (spoolLatch != null)
            {
               try
               {
                  spoolLatch.await(); // wait till the file will be done
               }
               catch (final InterruptedException e)
               {
                  // thinking that is ok, i.e. this thread is interrupted
                  throw new IOException("Swap file read error " + swapped.getAbsolutePath() + ". "
                     + e)
                  {
                     @Override
                     public Throwable getCause()
                     {
                        return e;
                     }
                  };
               }
            }
         }
         while (!swapped.spoolLatch.compareAndSet(null, new CountDownLatch(1)));
         return swapped;
      }
      else if (swappedRef != null)
      {
         // The SwapFile has been garbage collected so we remove it from the map
         CURRENT_SWAP_FILES.remove(absPath, swappedRef);
      }
      newsf.spoolLatch.set(new CountDownLatch(1));

      WeakReference<SwapFile> currentValue = CURRENT_SWAP_FILES.putIfAbsent(absPath, new WeakReference<SwapFile>(newsf));
      if (currentValue != null)
      {
         // the swap file has been put already so we need to loop
         return get(parent, child,cleaner);
      }
      return newsf;
   }

   /**
    * Tell if the file already spooled - ready for use.
    * 
    * @return boolean flag
    */
   public boolean isSpooled()
   {
      return spoolLatch.get() == null;
   }

   /**
    * Mark the file ready for read.
    */
   public void spoolDone()
   {
      final CountDownLatch sl = this.spoolLatch.get();
      this.spoolLatch.set(null);
      sl.countDown();
   }

   /**
    * Not applicable. Call get(File, String) method instead.
    * 
    * @throws IOException
    *           I/O error
    */
   public static SwapFile createTempFile(String prefix, String suffix, File directory) throws IOException
   {
      throw new IOException("Not applicable. Call get(File, String) method instead");
   }
   
   /**
    * {@inheritDoc}
    */
   @Override
   protected void finalize() throws Throwable
   {
      try
      {
         delete();
      }
      finally
      {
         super.finalize();
      }
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public boolean delete()
   {
      String path = this.getAbsolutePath();
      WeakReference<SwapFile> currentValue = CURRENT_SWAP_FILES.get(path);
      if (currentValue == null || (currentValue.get() == this || currentValue.get() == null))
      {
         CURRENT_SWAP_FILES.remove(path, currentValue);
         synchronized(this)
         {
            users.clear();
            final SpoolFile sf = this;

            if (sf.exists())
            {
               if (SwapFile.super.delete())
               {
                  return true;
               }
               else if (swapCleaner != null)
               {
                  swapCleaner.addFile(SwapFile.super.getAbsoluteFile());
               }
               if (LOG.isDebugEnabled())
               {
                  LOG.debug("Could not remove swap file on finalize : "
                                + SwapFile.super.getAbsoluteFile().getAbsolutePath());
               }
               return false;
            }
            return true;
         }
      }
      return false;
   }
}
