/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 * 
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License. You can obtain
 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

/*
 * FileUtils.java
 *
 * Created on November 16, 2001, 6:44 PM
 *
 * @author  bnevins
 *
 * Copyright 2000-2001 by iPlanet/Sun Microsystems, Inc.,
 * 901 San Antonio Road, Palo Alto, California, 94303, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information
 * of iPlanet/Sun Microsystems, Inc. ("Confidential Information").
 * You shall not disclose such Confidential Information and shall
 * use it only in accordance with the terms of the license
 * agreement you entered into with iPlanet/Sun Microsystems.
 *
 */

/*
 * KEDAR/MURALI has made some changes to this class
 * so that it works with installer(LogDomains esp.).
 */

package com.sun.enterprise.util.io;

import java.io.*;
import java.util.*;
//import com.sun.enterprise.util.diagnostics.Reporter;
//import com.sun.enterprise.util.StringUtils;
import com.sun.enterprise.util.OS;
//Bug 4677074 begin
import java.util.logging.Logger;
import java.util.logging.Level;
import java.util.jar.JarFile;
import java.util.jar.JarEntry;
//import com.sun.logging.LogDomains;
//Bug 4677074 end

public class FileUtils
{
    public static native int recursiveChownNative(String dirOrFile, String user);
    public static native int recursiveChownNative(String dirOrFile);
    public static native int recursiveChmodNative(String dirOrFile, String permissions);
//Bug 4677074 begin
    //static Logger _logger=LogDomains.getLogger(LogDomains.UTIL_LOGGER);
    //static Logger _logger=Logger.getLogger("javax.enterprise.system.util");
    static Logger _logger=IOLogger.getLogger();
    static Logger _utillogger=com.sun.logging.LogDomains.getLogger(com.sun.logging.LogDomains.UTIL_LOGGER);
//Bug 4677074 end
    private FileUtils()
    {
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static boolean safeIsDirectory(File f)
    {
        if(f == null || !f.exists() || !f.isDirectory())
            return false;
        
        return true;
    }
    
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static boolean safeIsRealDirectory(String s)
    {
        return safeIsRealDirectory(new File(s));
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static boolean safeIsRealDirectory(File f)
    {
        if(safeIsDirectory(f) == false)
            return false;
        
        
        
        // these 2 values while be different for symbolic links
        String canonical	= safeGetCanonicalPath(f);
        String absolute		= f.getAbsolutePath();
        
        if(canonical.equals(absolute))
            return true;
        
                /* Bug 4715043 -- WHOA -- Bug Obscura!!
                 * In Windows, if you create the File object with, say, "d:/foo", then the
                 * absolute path will be "d:\foo" and the canonical path will be "D:\foo"
                 * and they won't match!!!
                 **/
        if(OS.isWindows() && canonical.equalsIgnoreCase(absolute))
            return true;
        
        return false;
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static boolean safeIsDirectory(String s)
    {
        return safeIsDirectory(new File(s));
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static String safeGetCanonicalPath(File f)
    {
        if(f == null)
            return null;
        
        try
        {
            return f.getCanonicalPath();
        }
        catch(IOException e)
        {
            return f.getAbsolutePath();
        }
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static File safeGetCanonicalFile(File f)
    {
        
        if(f == null)
            return null;
        
        try
        {
            return f.getCanonicalFile();
        }
        catch(IOException e)
        {
            return f.getAbsoluteFile();
        }
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static boolean isEar(String filename)
    {
        return hasExtension(filename, ".ear");
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static boolean isJar(String filename)
    {
        return hasExtension(filename, ".jar");
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static boolean isZip(String filename)
    {
        return hasExtensionIgnoreCase(filename, ".zip");
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static boolean isArchive(String filename)
    {
        return isWar(filename) || isRar(filename) || isJar(filename) || isZip(filename) || isEar(filename);
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static boolean isArchive(File f)
    {
        return isWar(f) || isRar(f) || isJar(f) || isZip(f) || isEar(f);
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static boolean isWar(String filename)
    {
        return hasExtension(filename, ".war");
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static boolean isRar(String filename)
    {
        return hasExtension(filename, ".rar");
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static boolean isEar(File f)
    {
        return hasExtension(f, ".ear");
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static boolean isJar(File f)
    {
        return hasExtension(f, ".jar");
    }
    
    ///////////////////////////////////////////////////////////////////////////
    public static boolean isZip(File f)
    {
        return hasExtensionIgnoreCase(f, ".zip");
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static boolean isWar(File f)
    {
        return hasExtension(f, ".war");
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static boolean isRar(File f)
    {
        return hasExtension(f, ".rar");
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static boolean hasExtension(String filename, String ext)
    {
        if(filename == null || filename.length() <= 0)
            return false;
        
        return filename.endsWith(ext);
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static boolean hasExtension(File f, String ext)
    {
        if(f == null || !f.exists())
            return false;
        
        return f.getName().endsWith(ext);
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static boolean hasExtensionIgnoreCase(String filename, String ext)
    {
        if(filename == null || filename.length() <= 0)
            return false;
        
        return filename.toLowerCase().endsWith(ext.toLowerCase());
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static boolean hasExtensionIgnoreCase(File f, String ext)
    {
        if(f == null || !f.exists())
            return false;
        
        return f.getName().toLowerCase().endsWith(ext.toLowerCase());
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static boolean isLegalFilename(String filename)
    {
        if(!isValidString(filename))
            return false;
        
        for(int i = 0; i < ILLEGAL_FILENAME_CHARS.length; i++)
            if(filename.indexOf(ILLEGAL_FILENAME_CHARS[i]) >= 0)
                return false;
        
        return true;
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static boolean isFriendlyFilename(String filename)
    {
        if(!isValidString(filename))
            return false;
        
        if(filename.indexOf(BLANK) >= 0 || filename.indexOf(DOT) >= 0)
            return false;
        
        return isLegalFilename(filename);
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static String makeLegalFilename(String filename)
    {
        if(isLegalFilename(filename))
            return filename;
        
        for(int i = 0; i < ILLEGAL_FILENAME_CHARS.length; i++)
            filename = filename.replace(ILLEGAL_FILENAME_CHARS[i], REPLACEMENT_CHAR);
        
        return filename;
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static String makeFriendlyFileName(String filename)
    {
        // OK -- I hate having to go back in here to see if the 'N'
        // is uppercase or lowercase.  Let's have both!!
        return makeFriendlyFilename(filename);
    }
    
    ///////////////////////////////////////////////////////////////////////////
    public static String makeLegalNoBlankFileName(String filename)
    {
        return  makeLegalFilename(filename).replace(
            BLANK, REPLACEMENT_CHAR);
    }
    
    public static String makeFriendlyFilename(String filename)
    {
        if(isFriendlyFilename(filename))
            return filename;
        
        String ret = makeLegalNoBlankFileName(filename);
        // only replace the "." in the archive type suffix
        if (ret.endsWith(".ear"))
        {
            ret = ret.substring(0, ret.indexOf(".ear"));
            ret = ret + "_ear";
        }
        else if (ret.endsWith(".war"))
        {
            ret = ret.substring(0, ret.indexOf(".war"));
            ret = ret + "_war";
        }
        else if (ret.endsWith(".jar"))
        {
            ret = ret.substring(0, ret.indexOf(".jar"));
            ret = ret + "_jar";
        }
        else if (ret.endsWith(".rar"))
        {
            ret = ret.substring(0, ret.indexOf(".rar"));
            ret = ret + "_rar";
        }
        
        return ret;
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static String makeFriendlyFilenameNoExtension(String filename)
    {
        int index = filename.lastIndexOf('.');
        
        if(index > 0)
            filename = filename.substring(0, index);
        
        return(makeFriendlyFilename(filename));
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static String revertFriendlyFilenameExtension(String filename)
    {
        if (filename == null ||
            !(filename.endsWith("_ear") || filename.endsWith("_war") ||
            filename.endsWith("_jar") || filename.endsWith("_rar")) )
        {
            return filename;
        }
        
        String extension = "";
        if (filename.endsWith("_ear"))
        {
            filename = filename.substring(0, filename.indexOf("_ear"));
            extension = ".ear";
        }
        else if (filename.endsWith("_war"))
        {
            filename = filename.substring(0, filename.indexOf("_war"));
            extension = ".war";
        }
        else if (filename.endsWith("_jar"))
        {
            filename = filename.substring(0, filename.indexOf("_jar"));
            extension = ".jar";
        }
        else if (filename.endsWith("_rar"))
        {
            filename = filename.substring(0, filename.indexOf("_rar"));
            extension = ".rar";
        }
        return filename + extension;
    }
    
    public static String revertFriendlyFilename(String filename)
    {
        
        //first, revert the file extension
        String name = revertFriendlyFilenameExtension(filename);
        
        //then, revert the rest of the string
        return name.replace(REPLACEMENT_CHAR, '/');
    }
    
    
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static String makeFriendlyFileNameNoExtension(String filename)
    {
        // OK -- I hate having to go back in here to see if the 'N'
        // is uppercase or lowercase.  Let's have both!!
        return makeFriendlyFilenameNoExtension(filename);
    }
    
    /////////////////////////////////////////////////////////
    
    public static void liquidate(File parent)
    {
        whack(parent);
    }
    
    /////////////////////////////////////////////////////////
    
    /**
     *Deletes a directory and its contents.
     *<p>
     *If this method encounters a symbolic link in the subtree below "parent"
     *then it deletes the link but not any of the files pointed to by the link.
     *Note that whack will delete files if a symbolic link appears in the
     *path above the specified parent directory in the path.
     *@param parent the File at the top of the subtree to delete
     *@return success or failure of deleting the directory
     */
    public static boolean whack(File parent)
    {
        return whack(parent, null);
    }
    
    /**
     *Deletes a directory and its contents.
     *<p>
     *If this method encounters a symbolic link in the subtree below "parent"
     *then it deletes the link but not any of the files pointed to by the link.
     *Note that whack will delete files if a symbolic link appears in the
     *path above the specified parent directory in the path.
     *@param parent the File at the top of the subtree to delete
     *@return success or failure of deleting the directory
     */
    public static boolean whack(File parent, Collection<File> undeletedFiles)
    {
        try
        {
                /*
                 *Resolve any links up-stream from this parent directory and
                 *then whack the resulting resolved directory.
                 */
            return whackResolvedDirectory(parent.getCanonicalFile(), undeletedFiles);
        }
        catch (IOException ioe)
        {
            _utillogger.log(Level.SEVERE, "iplanet_util.io_exception", ioe);
            return false;
        }
    }
    
    /**
     *Deletes a directory and its contents.
     *<p>
     *The whackResolvedDirectory method is invoked with a File argument
     *in which any upstream file system links have already been resolved.
     *This method will treate Any file passed in that does not have the same
     *absolute and canonical path - as evaluated in safeIsRealDirectory -
     *as a link and will delete the link without deleting any files in the
     *linked directory.
     *
     *@param parent the File at the top of the subtree to delete
     *@return success or failure of deleting the directory
     */
    private static boolean whackResolvedDirectory(File parent, Collection<File> undeletedFiles)
    {
                /*
                 *Do not recursively delete the contents if the current parent
                 *is a symbolic link.
                 */
        if (safeIsRealDirectory(parent))
        {
            File[] kids = parent.listFiles();
            
            for(int i = 0; i < kids.length; i++)
            {
                File f = kids[i];
                
                if(f.isDirectory())
                    whackResolvedDirectory(f, undeletedFiles);
                else
                    if ( ! deleteFile(f) && undeletedFiles != null)
                    {
                    undeletedFiles.add(f);
                    }
                
            }
        }
        
                /*
                 *Delete the directory or symbolic link.
                 */
        return deleteFile(parent);
    }
    
    /**
     *Delete a file.  If on Windows and the delete fails, run the gc and retry the deletion.
     *@param file to delete
     *@return boolean indicating success or failure of the deletion atttempt; returns true if file is absent
     */
    public static boolean deleteFile(File f)
    {
            /*
             *The operation succeeds immediately if the file is deleted
             *successfully.  On systems that support symbolic links, the file
             *will be reported as non-existent if the file is a sym link to a
             *non-existent directory.  In that case invoke delete to remove
             *the link before checking for existence, since File.exists on
             *a symlink checks for the existence of the linked-to directory or
             *file rather than of the link itself.
             */
        if (f.delete())
        {
            return true;
        }
        
        boolean log = _utillogger.isLoggable(FILE_OPERATION_LOG_LEVEL);
        String filePath = f.getAbsolutePath();;
        
            /*
             *The deletion failed.  This could be simply because the file
             *does not exist.  In that case, log an appropriate message and
             *return.
             */
        if ( ! f.exists())
        {
            if (log)
            {
                _utillogger.log(Level.FINE, "enterprise_util.delete_failed_absent", filePath);
            }
            return true;
        }
        else
        {
                /*
                 *The delete failed and the file exists.  Log a message if that
                 *level is enabled and return false to indicate the failure.
                 */
            if (log)
            {
                _utillogger.log(FILE_OPERATION_LOG_LEVEL, "enterprise_util.error_deleting_file", filePath);
            }
            return false;
        }
    }
    
    /**
     *Attempts to delete files that could not be deleted earlier and were not overwritten.
     *<p>
     *On Windows, the method requests garbage collection which may unlock locked
     *files. (The JarFile finalizer closes the file.)
     *
     *On any platform, any remaining leftover files that cannot be cleaned
     *up are marked for delete-on-exit.
     *<p>
     *Note that the <code>leftovers</code> collection may be modified; a File
     *object is removed if that leftover file is deleted successfully.
     *@param leftovers a Collection of File objects for undeleted and non-overwritten files
     *@return whether or not all files were deleted successfully
     */
    public static boolean deleteLeftoverFiles(final Collection<File> leftovers)
    {
        if (leftovers.size() == 0)
        {
            return true;
        }
        
        /*
         *If all leftover files are removed then logging at the end of the method
         *(if the logging level is set fine enough) reports all the files that
         *were cleaned up.  To do that create a copy of the original leftover
         *files now because the logic below will remove items from the collection
         *as it successfully removes files.
         */
        Collection<File> originalLeftovers = null;
        boolean log = _utillogger.isLoggable(FILE_OPERATION_LOG_LEVEL);
        if (log)
        {
            originalLeftovers = new ArrayList<File>(leftovers);
        }
        
        /*
         *On Windows, as long as not all leftover files have been cleaned and we have not
         *run the max. number of retries, try again to trigger gc and delete
         *each remaining leftover file.
         */
        int retries = 0;
        if (OS.isWindows())
        {
            retries = doWithRetry(new RetriableWork()
            {
                public boolean workComplete()
                {
                    /*
                     *The removal of the leftover files is done if the collection
                     *is empty.
                     */
                    return leftovers.size() == 0;
                }
                
                public void run()
                {
                    /*
                     *Try to delete all remaining files.  For each successful deletion
                     *remove the corresponding File object from the leftovers collection.
                     */
                    for (Iterator<File> files = leftovers.iterator(); files.hasNext(); )
                    {
                        File f = files.next();
                        if (f.delete())
                        {
                            files.remove();
                        }
                    }
                }
            });
        }
        
        /*
         *Either all leftovers have been removed or at least one remains and we've
         *hit the retry limit...or this is not even a Windows platform.
         */
        if (leftovers.size() == 0 && log)
        {
            _utillogger.log(FILE_OPERATION_LOG_LEVEL, "enterprise_util.leftover_delete_success", new Object [] {
                Integer.valueOf(retries), formatFileCollection(originalLeftovers) } );
        }
        else
        {
            /*
             *Mark all remaining leftovers as delete-on-exit.
             */
            for (File f : leftovers)
            {
                f.deleteOnExit();
            }
            
            /*
             *No retries are performed on non-Windows systems, so log a message
             *that is appropriate to the platform.
             */
            if (OS.isWindows())
            {
                _utillogger.log(Level.WARNING, "enterprise_util.leftover_delete_failure_Windows", new Object [] {
                    Integer.valueOf(retries), formatFileCollection(leftovers)} );
            }
            else
            {
                _utillogger.log(Level.WARNING, "enterprise_util.leftover_delete_failure_nonWindows", new Object [] {
                    formatFileCollection(leftovers)} );
            }
        }
        
        return leftovers.size() == 0;
    }
    
    /**
     *Opens a stream to the specified output file, retrying if necessary.
     *@param out the output File for which a stream is needed
     *@return the FileOutputStream
     *@exception IOException for any errors opening the stream
     */
    public static FileOutputStream openFileOutputStream(File out) throws IOException
    {
        FileOutputStreamWork work = new FileOutputStreamWork(out);
        int retries = doWithRetry(work);
        if (work.workComplete())
        {
            return work.getStream();
        }
        else
        {
            IOException ioe = new IOException();
            ioe.initCause(work.getLastError());
            throw ioe;
        }
    }
    
    /**
     *Executes the supplied work object until the work is done or the max.
     *retry count is reached.
     *@param work the RetriableWork implementation to be run
     *@return the number of retries performed; 0 indicates the work succeeded without having to retry
     */
    private static int doWithRetry(RetriableWork work)
    {
        int retries = 0;
        
        /*
         *Try the work the first time.  Ideally this will work.
         */
        work.run();
        
        /*
         *If the work failed and this is Windows - on which running gc may
         *unlock the locked file - then begin the retries.
         */
        if ( ! work.workComplete() && OS.isWindows())
        {
            _utillogger.log(FILE_OPERATION_LOG_LEVEL, "enterprise_util.perform_gc");
            while ( ! work.workComplete() && retries++ < FILE_OPERATION_MAX_RETRIES)
            {
                try
                {
                    Thread.currentThread().sleep(FILE_OPERATION_SLEEP_DELAY_MS);
                }
                catch (InterruptedException ex)
                {
                }
                System.gc();
                work.run();
            }
        }
        return retries;
    }
    
    /**
     *Creates a String listing the absolute paths of files, separated by
     *the platform's line separator.
     *@param files the Collection of File objects to be listed
     *@return String containing the absolute paths of the files with the line separator between them
     */
    public static String formatFileCollection(Collection<File> files)
    {
        StringBuilder sb = new StringBuilder();
        String lineSep = System.getProperty("line.separator");
        String prefix = "";
        for (File f : files)
        {
            sb.append(prefix).append(f.getAbsolutePath());
            prefix = lineSep;
        }
        return sb.toString();
    }
    ///////////////////////////////////////////////////////////////////////////
    
    public static File getDirectory(File f)
    {
        String filename = f.getAbsolutePath();
        return new File((new File(filename)).getParent());
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static File createTempFile(File directory)
    {
        File f = null;
        
        try
        {
            f = File.createTempFile(TMPFILENAME, "jar", directory);
        }
        catch (IOException ioe)
        {
//Bug 4677074			ioe.printStackTrace();
//Bug 4677074 begin
            _logger.log(Level.SEVERE,"iplanet_util.io_exception",ioe);
//Bug 4677074 end
        }
        
        f.deleteOnExit(); // just in case
        return f;
    }
    
    /**
     * Returns an array of abstract pathnames that matches with the given
     * file extension. If the given abstract pathname does not denote a
     * directory, then this method returns null. If there is no matching
     * file under the given directory and its sub directories,
     * it returns null;
     *
     * @param    dirName    dir name under which search will begin
     *
     * @param    ext        file extension to look for
     *
     * @return   an array of abstract pathnames that matches with the extension
     */
    public static File[] listAllFiles(File dirName, String ext)
    {
        File[] target  = null;
        List list      = searchDir(dirName, ext);
        
        if ( (list != null) && (list.size() > 0) )
        {
            target = new File[list.size()];
            target = (File[]) list.toArray(target);
        }
        
        return target;
    }
    
    /**
     * Returns a list of abstract pathnames that matches with the given
     * file extension. If the given abstract pathname does not denote a
     * directory, then this method returns null. If there is no matching
     * file under the given directory and its sub directories, it returns
     * an empty list.
     *
     * @param    dirName    dir name under which search will begin
     * @param    ext        file extension to look for
     *
     * @return   a list of abstract pathnames of type java.io.File
     *           that matches with the given extension
     */
    public static List searchDir(File dirName, String ext)
    {
        List targetList = null;
        
        if (dirName.isDirectory())
        {
            targetList = new ArrayList();
            
            File[] list = dirName.listFiles();
            
            for (int i=0; i<list.length; i++)
            {
                if (list[i].isDirectory())
                {
                    targetList.addAll( searchDir(list[i], ext) );
                }
                else
                {
                    String name = list[i].toString();
                    if ( hasExtension(name, ext) )
                    {
                        targetList.add(list[i]);
                    }
                }
            }
        }
        
        return targetList;
    }
    
    /**
     * Copies a file.
     *
     * @param   from		Name of file to copy
     * @param   to			Name of new file
     * @exception  IOException  if an error while copying the content
     */
    public static void copy(String from, String to) throws IOException
    {
        //if(!StringUtils.ok(from) || !StringUtils.ok(to))
        if(from == null || to == null)
            throw new IllegalArgumentException("null or empty filename argument");
        
        File fin  = new File(from);
        File fout = new File(to);
        
        copy(fin, fout);
    }
    /**
     * Copies a file.
     *
     * @param   from    File to copy
     * @param   to		New file
     *
     * @exception  IOException  if an error while copying the content
     */
    public static void copy(File fin, File fout) throws IOException
    {
        if(safeIsDirectory(fin))
        {
            copyTree(fin, fout);
            return;
        }
        
        if(!fin.exists())
            throw new IllegalArgumentException("File source doesn't exist");
        
        //if(fout.exists() && overwrite == false)
        //throw new IOException("File destination exists, overwrite not specified");
        if(!safeIsDirectory(fout.getParentFile()))
            fout.getParentFile().mkdirs();
        
        copy(new FileInputStream(fin), new FileOutputStream(fout));
    }
    
    /**
     * Copies the entire tree to a new location.  Also copies all symlink directories.
     *
     * @param   sourceTree  File pointing at root of tree to copy
     * @param   destTree    File pointing at root of new tree
     *
     * @exception  IOException  if an error while copying the content
     */
    public static void copyTree(File din, File dout)
    throws IOException
    {
        copyTree(din, dout, true);
    }
    
    /**
     * Copies the entire tree to a new location.  
     * Symlink directories are only copied when copySymLinksDir is true.
     *
     * @param   sourceTree  File pointing at root of tree to copy
     * @param   destTree    File pointing at root of new tree
     * @param   copySymLinkDirs if true -- recursively copy symlink directories; otherwise ignore them.
     * @exception  IOException  if an error while copying the content
     */
    public static void copyTree(File din, File dout, boolean copySymLinkDirs)
    throws IOException
    {
        if(!safeIsDirectory(din))
            throw new IllegalArgumentException("Source isn't a directory");
        
        dout.mkdirs();
        
        if(!safeIsDirectory(dout))
            throw new IllegalArgumentException("Can't create destination directory");
        
        FileListerRelative flr = new FileListerRelative(din);
        flr.setCopySymLinks(copySymLinkDirs);     
        flr.setCopyEmptyDirs(true);
        String[] files = flr.getFiles();
        for(int i = 0; i < files.length; i++)
        {
            File fin  = new File(din, files[i]);
            File fout = new File(dout, files[i]);
            
            if(fin.isDirectory())
                fout.mkdirs();
            else
                copy(fin, fout);
            //System.out.println("Copied " + fin.getPath() + " to " + fout.getPath());
//Bug 4677074			System.out.print(".");
//Bug 4677074 begin
            //_logger.log(Level.FINE,"Copied " + fin.getPath() + " to "+ fout.getPath());
            _logger.log(Level.FINE,".");
//Bug 4677074 end
        }
    }
    
    /**
     * Copies the bytes from the given input stream to the output stream.
     * It closes the streams afterwards.
     *
     * @param   inStream    input stream from the src
     * @param   outStream   output stream to the destination
     *
     * @exception  IOException  if an error while copying the content
     */
    public static void copy(InputStream inStream, OutputStream outStream)
    throws IOException
    {
        copyWithoutClose(inStream, outStream);
        
        // closes the streams
        inStream.close();
        outStream.close();
    }
    
    /**
     * Copies the bytes from the given input stream to the output stream.
     * It does not close the streams afterwards.
     *
     * @param   inStream    input stream from the src
     * @param   outStream   output stream to the destination
     *
     * @exception  IOException  if an error while copying the content
     */
    public static void copyWithoutClose(InputStream inStream,
        OutputStream outStream) throws IOException
    {
        BufferedInputStream bis =
            new BufferedInputStream(inStream, BUFFER_SIZE);
        BufferedOutputStream bos =
            new BufferedOutputStream(outStream, BUFFER_SIZE);
        byte[] buf = new byte[BUFFER_SIZE];
        
        int len = 0;
        while (len != -1)
        {
            try
            {
                len = bis.read(buf, 0, buf.length);
            }
            catch (EOFException eof)
            {
                break;
            }
            
            if (len != -1)
            {
                bos.write(buf, 0, len);
            }
        }
        bos.flush();
    }
    
    /**
     * Returns a String with uniform slashes such that all the
     * occurances of '\\' are replaced with '/'.
     * In other words, the returned string will have all forward slashes.
     * Accepts non-null strings only.
     * @param a non null String
     * @return a String which <code> does not contain `\\` character </code>
     */
    public static String makeForwardSlashes(String inputStr)
    {
        if(inputStr == null)
            throw new IllegalArgumentException("null String FileUtils.makeForwardSlashes");
        return ( inputStr.replace('\\', '/') );
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    public static String getIllegalFilenameCharacters()
    {
        return ILLEGAL_FILENAME_STRING;
    }
    
    /**
     * This version will set the owner to the current user
     * @param dirOrFile - File or Directory to change owner
     * @throws NativeIOException if there was an error
     * @return a success String suitable for logging.
     */
    public static String recursiveChown(File dirOrFile) throws NativeIOException
    {
        return recursiveChown(dirOrFile, null);
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    /**
     * This version will set the owner to the specified user
     * @param dirOrFile - File or Directory to change owner
     * @throws NativeIOException if there was an error
     * @return a success String suitable for logging.
     */
    public static String recursiveChown(File dirOrFile, String user) throws NativeIOException
    {
        if(dirOrFile == null || !dirOrFile.exists())
            throw new NativeIOException("File doesn't exist: " + dirOrFile);
        
        String fname = dirOrFile.getAbsolutePath();
        int ret = 0;
        
        if(!isValidString(user))
            ret = recursiveChownNative(fname);
        else
            ret = recursiveChownNative(fname, user);
        
        NativeResults nr		= new NativeResults(ret);
        NativeIOException ne	= nr.getResultException();
        
        if(ne == null)
            return nr.getResultString();
        else
            throw ne;
    }
    /**
     * Recursively changes the file permissions via native code.
     * @param dirOrFile the directory or file to chmod (recursively if directory)
     * @param permissions a String representing permissions.
     * <p> permissions must be exactly like so:
     * "rwxrwxrwx" which is user-group-others
     * put a dash (or anything else) in a position that you don't want the permission set.
     * e.g. rwx -> user, r -> group, nothing -> others
     * "rwxr-----"
     * @throws NativeIOException -- if there was an error
     * @return String suitable for logging about the success
     */
    public static String recursiveChmod(File dirOrFile, String permissions) throws NativeIOException
    {
        if(dirOrFile == null || !dirOrFile.exists())
            throw new NativeIOException("File doesn't exist: " + dirOrFile);
        
        if(permissions == null)
            throw new NativeIOException("null permissions string.");
        
        permissions = permissions.toLowerCase(); // using R,W,X should not be an error!
        verifyPermissions(permissions);
        
        int ret = recursiveChmodNative(dirOrFile.getAbsolutePath(), permissions);
        
        NativeResults nr		= new NativeResults(ret);
        NativeIOException ne	= nr.getResultException();
        
        if(ne == null)
            return nr.getResultString();
        else
            throw ne;
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    static boolean isValidString(String s)
    {
        return ((s != null) && (s.length() != 0));
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    /**
     * Windows has BIG issues renaming a directory that is open somnewhere -- e.g. if
     * a DOS box is opened anywhere in that directory.
     * This method will try to do a "virtual renaming" if there are problems
     * I.e. it attempts to do a simple rename, if that fails it will copy everything under
     * the original directory to the renamed directory.  Then it will delete everything
     * under the original directory that the OS will allow it to.
     * NOTE: if the proposed new name already exists - I will try to whack it.
     * bnevins April 2003
     * @returns the renamed directory-name
     */
    
    public static File smartRename(File original) throws IOException
    {
        if(original == null)
            throw new IllegalArgumentException("null argument");
        
        String originalName = original.getAbsolutePath();
        
        if(!safeIsDirectory(original))
            throw new IOException("Directory doesn't exist: " + originalName);
        
        // let's try to work with "_old" added to name...
        
        String renamedName	= originalName + "_old";
        File renamed		= new File(renamedName);
        
        if(renamed.exists())
        {
            whack(renamed);
            
            // it might have failed:
            if(renamed.exists())
            {
                for(int i = 0; i < 1000; i++)
                {
                    String name = renamedName + i;
                    renamed = new File(name);
                    
                    try
                    { whack(renamed); }
                    catch(Throwable t)
                    { /* ignore!! */ }
                    
                    if(!renamed.exists())
                    {
                        // found one!!!
                        renamedName = name;
                        break;
                    }
                }
                // once more in case we went through all 1000!
                if(renamed.exists())
                    throw new IOException("Tried 1000 possible rename directory names.  None worked!");
            }
        }
        
        // whew!
        
        // try a simple rename...
        
        if(original.renameTo(renamed))
            return renamed;
        
        // OK -- we'return probably in Windows and something is referencing original.
        
        copyTree(original, renamed);
        whack(original);	// this will delete whatever is possible to delete...
        
        return renamed;
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    /**
     *Rename, running gc on Windows if needed to try to force open streams to close.
     *@param file to be renamed
     *@param new name for the renamed file
     *@return boolean result of the rename attempt
     */
    public static boolean renameFile(File fromFile, File toFile)
    {
        boolean log = _utillogger.isLoggable(FILE_OPERATION_LOG_LEVEL) || _utillogger.isLoggable(Level.FINE);
        
        RenameFileWork renameWork = new RenameFileWork(fromFile, toFile);
        int retries = doWithRetry(renameWork);
        boolean result = renameWork.workComplete();
        
        String fromFilePath = null;
        String toFilePath = null;
        if (log || ! result)
        {
            fromFilePath = fromFile.getAbsolutePath();
            toFilePath = toFile.getAbsolutePath();
        }
        
            /*
             *If the rename worked, then write an appropriate log message if the
             *logging level allows.
             */
        if (result)
        {
            if (log)
            {
                    /*
                     *If the rename worked without retries, then log a FINE message.
                     *If retries were needed then use the configured
                     *FILE_OPERATION_LOG_LEVEL.
                     */
                if (retries == 0)
                {
                    if (_utillogger.isLoggable(Level.FINE))
                    {
                        _utillogger.log(Level.FINE, "enterprise_util.rename_initial_success", new Object [] {
                            fromFilePath, toFilePath } );
                    }
                }
                else
                {
                    _utillogger.log(FILE_OPERATION_LOG_LEVEL, "enterprise_util.retry_rename_success", new Object []
                    { fromFilePath, toFilePath, Integer.valueOf(retries) } );
                }
            }
        }
        else
        {
                /*
                 *The rename has failed.  Write a warning message.
                 */
            _utillogger.log(Level.WARNING, "enterprise_util.retry_rename_failure", new Object []
            { fromFilePath, toFilePath, Integer.valueOf(retries) } );
        }
        return result;
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    private static void verifyPermissions(String per) throws NativeIOException
    {
        // brace yourself for some involved argument checking.
        // It's not pretty but it *is* neccessary.
        
        if(per == null)
            throw new NativeIOException("null permissions string.");
        if(per.length() != 9)
            throw new NativeIOException("permissions string must be exactly" +
                " 9 characters long.  It is " + per.length() + " characters long.");
        
        // make sure every character is r, w, x, or -
        String err1 = "permissions string has a bad character ('";
        String err2 = "') at position #";
        String err3 = ".  Expected '-' or '";
        String err4 = "'";
        
        for(int i = 0; i < 9; i++)
        {
            
            char c = per.charAt(i);
            int pos = i % 3;	// 0->r, 1->w, 2->x
            
            if(pos == 0)
            {
                if(c != 'r' && c != '-')
                    throw new NativeIOException(err1 + c + err2 + (i + 1) + err3 + 'r' + err4);
            }
            else if(pos == 1)
            {
                if(c != 'w' && c != '-')
                    throw new NativeIOException(err1 + c + err2 + (i + 1) + err3 + 'w' + err4);
            }
            else if(pos == 2)
            {
                if(c != 'x' && c != '-')
                    throw new NativeIOException(err1 + c + err2 + (i + 1) + err3 + 'x' + err4);
            }
        }
    }
    
    /** Appends the given line at the end of given text file. If the given
     * file does not exist, an attempt is made to create it.
     * Note that this method can handle only text files.
     * @param fileName name of the text file that needs to be appended to
     * @param line the line to append to
     * @throws RuntimeException in case of any error - that makes it callable
     * from a code not within try-catch. Note that NPE will be thrown if either
     * argument is null.
     * Note that this method is not tested with String containing characters
     * with 2 bytes.
     */
    public static void appendText(String fileName, String line) throws
        RuntimeException
    {
        RandomAccessFile file = null;
        try
        {
            final String MODE = "rw";
            file = new RandomAccessFile(fileName, MODE);
            file.seek(file.getFilePointer() + file.length());
            file.writeBytes(line);
        }
        catch(Exception e)
        {
            throw new RuntimeException("FileUtils.appendText()", e);
        }
        finally
        {
            try
            {
                if (file != null)
                    file.close();
            }
            catch(Exception e)
            {}
        }
    }
    
    public static void appendText(String fileName, StringBuffer buffer)
    throws IOException, FileNotFoundException
    {
        appendText(fileName, buffer.toString());
    }
    ///////////////////////////////////////////////////////////////////////////
    
    /** A utility routine to read a <b> text file </b> efficiently and return
     * the contents as a String. Sometimes while reading log files of spawned
     * processes this kind of facility is handy. Instead of opening files, coding
     * FileReaders etc. this method could be employed. It is expected that the
     * file to be read is <code> small </code>.
     * @param fileName String representing absolute path of the file
     * @return String representing the contents of the file, empty String for an empty file
     * @throws java.io.IOException if there is an i/o error.
     * @throws java.io.FileNotFoundException if the file could not be found
     */
    public static String readSmallFile(final String fileName) throws IOException, FileNotFoundException
    {
        return (readSmallFile(new File(fileName)) );
    }
    
    public static String readSmallFile(final File file) throws IOException
    {
        final BufferedReader bf = new BufferedReader(new FileReader(file));
        final StringBuilder sb = new StringBuilder(); //preferred over StringBuffer, no need to synchronize
        String line = null;
        try
        {
            while ( (line = bf.readLine()) != null )
            {
                sb.append(line);
                sb.append(System.getProperty("line.separator"));
            }
        }
        finally
        {
            try
            {
                bf.close();
            }
            catch (Exception e)
            {}
        }
        return ( sb.toString() );
    }
    
    public static void main(String[] args)
    {
        try
        {
            System.out.println(smartRename(new File("C:/temp/test")));
        }
        catch(Throwable t)
        {
            t.printStackTrace();
        }
                /*
                try
                {
                        String ret = recursiveChmod(new File("c:/temp"), "rwxr-----");
                        System.out.println("recursiveChmod returned: " + ret);
                }
                catch(Throwable t)
                {
                        t.printStackTrace();
                }
                //_logger.log(Level.SEVERE,"iplanet_util.io_exception", new IOException());
                //System.out.println("xxxxxxxxxx");
                 **/
        appendText("empty.txt", "text line");
    }
    
    /**
     * Method to read a file and return it as String
     */
    public static String getFileContents(String fileName)
    throws IOException, FileNotFoundException
    {
        
        FileReader fr = null;
        fr = new FileReader(fileName);
        StringWriter sr = new StringWriter();
        
        try
        {
            char[] buf = new char[1024];
            int len = 0;
            while (len != -1)
            {
                try
                {
                    len = fr.read(buf, 0, buf.length);
                }
                catch (EOFException eof)
                {
                    break;
                }
                if (len != -1)
                {
                    sr.write(buf, 0, len);
                }
            }
            
            fr.close();
            sr.close();
            
            return sr.toString();
            
        }
        finally
        {
            if (fr != null)
            {
                try
                {
                    fr.close();
                }
                catch (IOException ioe)
                {
                }
            }
        }
    }
    
    /**
     *Make sure the specified path is a legal length on Windows.
     *@param fullPath the String holding the path to check
     *@throws IOException if the path is too long
     */
    public static void validateWindowsFilePathLength(String fullPath) throws IOException
    {
        /*
         *No longer needed because 1.5.0-06 now avoids the Windows file path
         *length limitation.
         */
        return;
    }
    
    /**
     *Make sure the specified path is a legal length on Windows.
     *@param file the File to check
     *@throws IOException if the path is too long
     */
    public static void validateWindowsFilePathLength(File file) throws IOException
    {
        validateWindowsFilePathLength(file.getAbsolutePath());
    }
    
    
    /**
     *Represents a unit of work that should be retried, if needed, until it
     *succeeds or the configured retry limit is reached.
     *<p>
     *The <code>run</code> method required by the Runnable interface is invoked
     *to perform the work.
     */
    private interface RetriableWork extends Runnable
    {
        
        /**
         *Returns whether the work to be done by this instance of RetriableWork
         *has been completed successfully.
         *<p>
         *This method may be invoked multiple times and so should not have
         *side effects.
         *@return whether the work has been successfully completed
         */
        public boolean workComplete();
    }
    
    /**
     *Retriable work for renaming a file.
     */
    private static class RenameFileWork implements RetriableWork
    {
        
        private File originalFile;
        private File newFile;
        private boolean renameResult = false;
        
        public RenameFileWork(File originalFile, File newFile)
        {
            this.originalFile = originalFile;
            this.newFile = newFile;
        }
        
        public boolean workComplete()
        {
            return renameResult;
        }
        
        public void run()
        {
            renameResult = originalFile.renameTo(newFile);
        }
    }
    
    /**
     *Retriable work for opening a FileOutputStream.
     */
    private static class FileOutputStreamWork implements RetriableWork
    {
        
        private FileOutputStream fos = null;
        private Throwable lastError = null;
        private File out;
        
        public FileOutputStreamWork(File out)
        {
            this.out = out;
        }
        
        public boolean workComplete()
        {
            return fos != null;
        }
        
        public void run()
        {
            try
            {
                fos = new FileOutputStream(out);
                lastError = null;
            }
            catch (IOException ioe)
            {
                lastError = ioe;
            }
        }
        
        public FileOutputStream getStream()
        {
            return fos;
        }
        
        public Throwable getLastError()
        {
            return lastError;
        }
    }
    
    
    ///////////////////////////////////////////////////////////////////////////
    
    private static final int BUFFER_SIZE = 0x10000; // 64k
    private final static	char[]	ILLEGAL_FILENAME_CHARS	= {'/', '\\', ':', '*', '?', '"', '<', '>', '|' };
    private final static	String	ILLEGAL_FILENAME_STRING	= "\\/:*?\"<>|";
    private final static	char	REPLACEMENT_CHAR		= '_';
    private final static	char	BLANK					= ' ';
    private final static	char	DOT						= '.';
    private static			String	TMPFILENAME				= "scratch";
        /*
         *The following property names are private, unsupported, and unpublished.
         */
    private static final int FILE_OPERATION_MAX_RETRIES = Integer.getInteger("com.sun.appserv.winFileLockRetryLimit", 4).intValue();
    private static final int FILE_OPERATION_SLEEP_DELAY_MS = Integer.getInteger("com.sun.appserv.winFileLockRetryDelay", 100).intValue();
    private static final Level FILE_OPERATION_LOG_LEVEL = Level.FINE;
}
