/* $Id$ */

/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.manifoldcf.core.lockmanager;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;

import org.apache.manifoldcf.core.interfaces.ManifoldCFException;
import org.apache.manifoldcf.core.system.ManifoldCF;

/** This is the file-based lock manager.
*/
public class FileLockManager extends BaseLockManager
{
  public static final String _rcsid = "@(#)$Id$";

  /** Synchronization directory property - local to this implementation of ILockManager */
  public static final String synchDirectoryProperty = "org.apache.manifoldcf.synchdirectory";

  // These are for file-based locks (which cross JVM boundaries)
  protected final static Integer lockPoolInitialization = new Integer(0);
  protected static LockPool myFileLocks = null;

  // This is the directory used for cross-JVM synchronization, or null if off
  protected File synchDirectory = null;

  public FileLockManager(File synchDirectory)
    throws ManifoldCFException
  {
    this.synchDirectory = synchDirectory;
    if (synchDirectory == null)
      throw new ManifoldCFException("Synch directory cannot be null");
    if (!synchDirectory.isDirectory())
      throw new ManifoldCFException("Synch directory must point to an existing, writeable directory!",ManifoldCFException.SETUP_ERROR);
    synchronized(lockPoolInitialization)
    {
      if (myFileLocks == null)
      {
        myFileLocks = new LockPool(new FileLockObjectFactory(synchDirectory));
      }
    }
  }
  
  public FileLockManager()
    throws ManifoldCFException
  {
    this(getSynchDirectoryProperty());
  }

  /** Get the synch directory property. */
  public static File getSynchDirectoryProperty()
    throws ManifoldCFException
  {
    return ManifoldCF.getFileProperty(synchDirectoryProperty);
  }
  
  /** Calculate the name of a flag resource.
  *@param flagName is the name of the flag.
  *@return the name for the flag resource.
  */
  protected static String getFlagResourceName(String flagName)
  {
    return "flag-"+flagName;
  }
    
  /** Raise a flag.  Use this method to assert a condition, or send a global signal.  The flag will be reset when the
  * entire system is restarted.
  *@param flagName is the name of the flag to set.
  */
  @Override
  public void setGlobalFlag(String flagName)
    throws ManifoldCFException
  {
    String resourceName = getFlagResourceName(flagName);
    String path = makeFilePath(resourceName);
    (new File(path)).mkdirs();
    File f = new File(path,ManifoldCF.safeFileName(resourceName));
    try
    {
      f.createNewFile();
    }
    catch (InterruptedIOException e)
    {
      throw new ManifoldCFException("Interrupted: "+e.getMessage(),e,ManifoldCFException.INTERRUPTED);
    }
    catch (IOException e)
    {
      throw new ManifoldCFException(e.getMessage(),e);
    }
  }

  /** Clear a flag.  Use this method to clear a condition, or retract a global signal.
  *@param flagName is the name of the flag to clear.
  */
  @Override
  public void clearGlobalFlag(String flagName)
    throws ManifoldCFException
  {
    String resourceName = getFlagResourceName(flagName);
    File f = new File(makeFilePath(resourceName),ManifoldCF.safeFileName(resourceName));
    f.delete();
  }
  
  /** Check the condition of a specified flag.
  *@param flagName is the name of the flag to check.
  *@return true if the flag is set, false otherwise.
  */
  @Override
  public boolean checkGlobalFlag(String flagName)
    throws ManifoldCFException
  {
    String resourceName = getFlagResourceName(flagName);
    File f = new File(makeFilePath(resourceName),ManifoldCF.safeFileName(resourceName));
    return f.exists();
  }

  /** Read data from a shared data resource.  Use this method to read any existing data, or get a null back if there is no such resource.
  * Note well that this is not necessarily an atomic operation, and it must thus be protected by a lock.
  *@param resourceName is the global name of the resource.
  *@return a byte array containing the data, or null.
  */
  @Override
  public byte[] readData(String resourceName)
    throws ManifoldCFException
  {
    File f = new File(makeFilePath(resourceName),ManifoldCF.safeFileName(resourceName));
    try
    {
      InputStream is = new FileInputStream(f);
      try
      {
        ByteArrayBuffer bab = new ByteArrayBuffer();
        while (true)
        {
          int x = is.read();
          if (x == -1)
            break;
          bab.add((byte)x);
        }
        return bab.toArray();
      }
      finally
      {
        is.close();
      }
    }
    catch (FileNotFoundException e)
    {
      return null;
    }
    catch (InterruptedIOException e)
    {
      throw new ManifoldCFException("Interrupted: "+e.getMessage(),e,ManifoldCFException.INTERRUPTED);
    }
    catch (IOException e)
    {
      throw new ManifoldCFException("IO exception: "+e.getMessage(),e);
    }
  }
  
  /** Write data to a shared data resource.  Use this method to write a body of data into a shared resource.
  * Note well that this is not necessarily an atomic operation, and it must thus be protected by a lock.
  *@param resourceName is the global name of the resource.
  *@param data is the byte array containing the data.  Pass null if you want to delete the resource completely.
  */
  @Override
  public void writeData(String resourceName, byte[] data)
    throws ManifoldCFException
  {
    try
    {
      String path = makeFilePath(resourceName);
      // Make sure the directory exists
      (new File(path)).mkdirs();
      File f = new File(path,ManifoldCF.safeFileName(resourceName));
      if (data == null)
      {
        f.delete();
        return;
      }
      FileOutputStream os = new FileOutputStream(f);
      try
      {
        os.write(data,0,data.length);
      }
      finally
      {
        os.close();
      }
    }
    catch (InterruptedIOException e)
    {
      throw new ManifoldCFException("Interrupted: "+e.getMessage(),e,ManifoldCFException.INTERRUPTED);
    }
    catch (IOException e)
    {
      throw new ManifoldCFException("IO exception: "+e.getMessage(),e);
    }
  }

  /** Override this method to change the nature of global locks.
  */
  @Override
  protected LockPool getGlobalLockPool()
  {
    return myFileLocks;
  }

  /** Create a file path given a key name.
  *@param key is the key name.
  *@return the file path.
  */
  protected String makeFilePath(String key)
  {
    int hashcode = key.hashCode();
    int outerDirNumber = (hashcode & (1023));
    int innerDirNumber = ((hashcode >> 10) & (1023));
    String fullDir = synchDirectory.toString();
    if (fullDir.length() == 0 || !fullDir.endsWith("/"))
      fullDir = fullDir + "/";
    fullDir = fullDir + Integer.toString(outerDirNumber)+"/"+Integer.toString(innerDirNumber);
    return fullDir;
  }

  protected static final int BASE_SIZE = 128;
  
  protected static class ByteArrayBuffer
  {
    protected byte[] buffer;
    protected int length;
    
    public ByteArrayBuffer()
    {
      buffer = new byte[BASE_SIZE];
      length = 0;
    }
    
    public void add(byte b)
    {
      if (length == buffer.length)
      {
        byte[] oldbuffer = buffer;
        buffer = new byte[length * 2];
        System.arraycopy(oldbuffer,0,buffer,0,length);
      }
      buffer[length++] = b;
    }
    
    public byte[] toArray()
    {
      byte[] rval = new byte[length];
      System.arraycopy(buffer,0,rval,0,length);
      return rval;
    }
  }

}
