/*
** Module   : FileSystem.java
** Abstract : utility/helper methods for handling common file system tasks
**
** Copyright (c) 2004-2009, Golden Code Development Corporation.
**
** -#- -I- --Date-- -----------------------Description------------------------
** 001 GES 20090204 First version based on prior code from the Utils class.
**                  Provides utility/helper methods for handling common file
**                  system tasks. Added safe filename creation.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program 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 Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License
** along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package com.goldencode.io;

import java.io.*;

/**
 * Provides utility/helper methods for handling common file system tasks.
 */
public final class FileSystem
{
   /**
    * Private constructor to prevent instances of this class from being
    * created.
    */
   private FileSystem()
   {
   }
   
   /**
    * Convert the input text into a safe filename. Extra spaces are trimmed,
    * single spaces are converted to underscores and other non-safe characters
    * are removed.
    * <p>
    * <b>Warning: this method is "lossy" so it may generate the same name from
    * more than one different input.</b>
    *
    * @param    input
    *           Input (possibly unsafe) file name. Do NOT include the file
    *           separator character in this text, as such characters are
    *           dropped. This name must be only a single segment of the
    *           path or the specific file name itself.
    *
    * @return   A safe filename. If <code>null</code> or the empty string
    *           is provided on input, an empty string will be returned.
    */
   public static String makeSafeFilename(String input)
   {
      if (input == null || input.length() == 0)
         return "";
      
      StringBuilder sb = new StringBuilder();
      
      // remove leading, trailing whitespace
      input = input.trim();
      
      int     len      = input.length();
      boolean removeWS = false;
      char    last     = 0;
      char    ch       = 0;
      
      for (int i = 0; i < len; i++)
      {
         last = ch;
         ch = input.charAt(i);
         
         if (Character.isWhitespace(ch))
         {
            if (removeWS)
            {
               // drop the char
               continue;
            }
            else
            {
               // first whitespace converts to underscore if there wasn't
               // already an underscore in the previous character
               if (last != '_')
                  sb.append('_');
               
               // remove following whitespace chars
               removeWS = true;
            }
         }
         else
         {
            // reset the whitespace removal flag
            removeWS = false;
            
            // only append safe characters (all others are dropped)
            if (Character.isLetterOrDigit(ch) ||
                ch == '-'                     ||
                ch == '_'                     ||
                ch == '.')
            {
               sb.append(ch);
            }
         }
      }
      
      return sb.toString();
   }
   
   /**
    * Creates all subdirectories needed to provide a valid path matching the
    * given path.
    *
    * @param    path
    *           Directory to be created.
    *
    * @return   <code>true</code> if the full necessary path already
    *           exists or if it has been created, <code>false</code>
    *           otherwise.
    */
   public static boolean createPath(String path)
   {
      File file = new File(path);
      
      // anything to do?
      if (file.exists() && file.isDirectory())
         return true;
      
      try
      {
         // best effort to create the directories including all needed
         // parent dirs
         file.mkdirs();
      }
      catch (Exception exc)
      {
         // some failure, probably a security violation
         return false;
      }
      
      // it better be there now...
      if (file.exists() && file.isDirectory())
         return true;

      // some other unexpected problem!
      return false;
   }
   
   /**
    * Creates all subdirectories needed to provide a valid path for the
    * target filename that is passed in.  It is expected that there will 
    * always be a path included in this filename such that making 
    * directories makes sense.
    *
    * @param    tar
    *           Target filename for which paths must be created.
    *
    * @return   <code>true</code> if the full necessary path already
    *           exists or if it has been created, <code>false</code>
    *           otherwise.
    */
   public static boolean createPathForTarget(String tar)
   {
      String path = tar.substring(0, tar.lastIndexOf(File.separatorChar));
      return createPath(path);
   }
   
   /**
    * Find the specified file in the file system using the specified search
    * paths and return it. Handles absolute and relative paths.
    *
    * @param   filename
    *          Name of the source file to find; if this is not an absolute
    *          path, it should be relative to one of the paths in the
    *          <code>searchPaths</code> array.
    * @param   paths
    *          Array of absolute paths which are prepended to a relative
    *          <code>filename</code>.
    *
    * @return  The file if it was found.
    *
    * @throws  FileNotFoundException
    *          if a file named <code>filename</code> does not exist in the
    *          specified search paths (or at the absolute path provided).
    */
   public static File findFileInPath(String filename, String[] paths)
   throws FileNotFoundException
   {
      File file = new File(filename);
      
      if (file.isAbsolute())
      {
         // Return file if it is absolute and exists.
         if (file.exists())
         {
            return file;
         }
      }
      else
      {
         // Use search paths to resolve relative references.
         int len = paths.length;
         for (int i = 0; i < len; i++)
         {
            file = new File(paths[i], filename);
            if (file.exists())
            {
               return file;
            }
         }
      }
      
      // Reassemble path for error message.
      StringBuffer buf = new StringBuffer();
      String sep = File.pathSeparator;
      int len = paths.length;
      for (int i = 0; i < len; i++)
      {
         buf.append(paths[i])
            .append(sep);
      }
      
      throw new FileNotFoundException(
         "File '" + filename + "' not found in path: " + buf);
   }
      
   /**
    * Determines if a both source and target exist as files and if the target
    * has a later timestamp.  It does not consider contents or size in this
    * determination.
    *
    * @param    source
    *           Source filename.
    * @param    target
    *           Target filename.
    *
    * @return   <code>true</code> if the both files exist and the target is
    *           newer, otherwise <code>false</code> (includes failures).
    */
   public static boolean isLaterVersion(String source, String target)
   {      
      File src = new File(source);
      File tar = new File(target);
      
      // make sure the source file is OK
      boolean srcOK = src.exists() && src.isFile();
      
      if (!srcOK)
         return false;
      
      // make sure the target file is not there or if it is there that
      // it is older than the source
      if (tar.exists())
      {
         if (tar.isFile())
         {
            // returns true if the source is older, false if the target is
            // older
            return src.lastModified() < tar.lastModified();
         }
      }
      
      return false;
   }     
   
   /**
    * Creates the directory tree to the target if needed and then copies
    * the source to the target if needed.  The copy is byte for byte, no
    * modifications of the file will occur.
    *
    * @param    src
    *           Source filename.
    * @param    tar
    *           Target filename.
    *
    * @return   <code>true</code> if the file was copied successfully OR
    *           if the file didn't need to be copied, <code>false</code>
    *           on any failure.
    */
   public static boolean simpleCopy(String src, String tar)
   {
      // try to create the directory
      if (!createPathForTarget(tar))
      {
         // dir creation failed
         return false;
      }
      
      return copyFileIfNeeded(src, tar, 0);
   }
   
   /**
    * Conditionally copies a file, byte for byte (no changes) to a new file,
    * if and only if the source is an accessible file and the target doesn't
    * exist or the target exists as a file but is older than the source or
    * has a different size.
    *
    * @param    source
    *           Source filename.
    * @param    target
    *           Target filename.
    * @param    bsize
    *           Buffer size or if 0 the buffer size will default to the file
    *           size or 64Kb whichever is less.
    *
    * @return   <code>true</code> if the file was copied successfully OR if
    *           the file didn't need to be copied, <code>false</code> on any
    *           failure.
    */
   public static boolean copyFileIfNeeded(String source,
                                          String target,
                                          int    bsize)
   {      
      File src = new File(source);
      File tar = new File(target);
      
      // make sure the source file is OK
      boolean srcOK = src.exists() && src.isFile();
      
      if (!srcOK)
         return false;
      
      boolean tarOK = true; 
      
      // make sure the target file is not there or if it is there that
      // it is older than the source or it is not the same size, before
      // trying to copy
      if (tar.exists())
      {
         if (tar.isFile())
         {
            boolean oldSrc = src.lastModified() > tar.lastModified();
            long    diff   = src.length() - tar.length();               
            tarOK = oldSrc || (diff != 0);
         }
         else
         {
            return false;
         }
      }
      
      boolean result = false;
      
      // already checked src, now check target
      if (tarOK)
      {
         // best efforts attempt to copy
         result = copyFile(source, target, 0, false);
      }
      
      return result;
   }   
   
   /**
    * Handles the byte for byte copying of a single source file to a target 
    * file, using a buffer size of the caller's choice. The resulting target
    * file will be a newly created file (date/timestamps will be new).
    *
    * @param    source
    *           Source filename.
    * @param    target
    *           Target filename.
    * @param    bsize
    *           Buffer size or if 0 the buffer size will default to the file
    *           size or 64Kb whichever is less.
    * @param    append
    *           <code>true</code> if the output file should be opened in
                append mode.
    *
    * @return   <code>true</code> if the file was copied successfully,
    *           <code>false</code> on any failure.
    */
   public static boolean copyFile(String  source,
                                  String  target,
                                  int     bsize,
                                  boolean append)                                  
   {
      File in  = new File(source);
      long len = in.length();
      
      // if an invalid buffer size is set, the lesser of 64Kb or the file
      // length is used
      if (bsize < 1)
      {
         bsize = 64*1024;
         
         if (len < bsize)
            bsize = (int) len;
      }
      
      try
      {
         FileInputStream  src = new FileInputStream(in);
         FileOutputStream tar = new FileOutputStream(target, append);
         
         int off = 0;
         
         byte[] buf = new byte[bsize];
   
         while (len > bsize)
         {
            src.read(buf, off, bsize);
            tar.write(buf, 0, bsize);
            len -= bsize;
         }
         
         src.read(buf, off, (int)len);
         tar.write(buf, 0, (int)len);
         
         src.close();
         tar.close();
      }
      catch (Exception exc)
      {
         return false;
      }
      
      return true;
   }
}
