/*
* $Id$
*
* Copyright 2006, The jCoderZ.org Project. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials
* provided with the distribution.
* * Neither the name of the jCoderZ.org Project nor the names of
* its contributors may be used to endorse or promote products
* derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.jcoderz.commons.connector.file;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.resource.ResourceException;
import javax.resource.spi.SecurityException;
import org.jcoderz.commons.connector.ConnectionBase;
import org.jcoderz.commons.connector.ConnectionNotificationListener;
import org.jcoderz.commons.util.IoUtil;
/**
* Implements the File System Connection.
* This connection handle provides physical interraction to the File System.
* This behavior is not really conform to the JCA specification, but satisfy
* requirenments to provide set of base operations on the underlying File
* System.
*
*/
class FsConnectionImpl
extends ConnectionBase
implements FsConnection
{
public static final String TMP_FILE_PREFIX = "FWK";
public static final String TMP_FILE_SUFFIX = "fwk";
/** The full qualified name of this class. */
private static final String CLASSNAME = FsConnectionImpl.class.getName();
/** The logger to use. */
private static final Logger logger = Logger.getLogger(CLASSNAME);
/** Random instance to use for generating of backup file names. */
private static final Random RANDOM = new Random();
/** Helds closable instances. */
private final Set mCloseables = new HashSet();
/** Random handle for debug logging. */
private final int mIndex;
/** To string holder. */
private final String mStringified;
/** The file temp dir. **/
private final File mTmpDir;
private final long mFileTransferChunkSize;
/**
* Constructor.
* @param cnl The listener interesting on this connection.
* @param props Configuration properties.
*/
public FsConnectionImpl (ConnectionNotificationListener cnl,
Properties props)
{
super(cnl);
synchronized (RANDOM)
{
mIndex = RANDOM.nextInt(Integer.MAX_VALUE);
mStringified = "FsConnectionImpl[" + mIndex + "]";
}
String tmp = null;
if (props != null)
{
tmp = props.getProperty(FsConnectionFactory.PROP_TEMP_DIR);
}
if (tmp == null)
{
tmp = System.getProperty("java.io.tmpdir");
}
mTmpDir = new File(tmp);
if (props != null)
{
mFileTransferChunkSize
= Long.getLong(FsConnectionFactory.PROP_FILE_TRANSFER_CHUNK_SIZE,
FsConnectionFactory.FILE_TRANSFER_CHUNK_SIZE_DEF_VALUE)
.longValue();
}
else
{
mFileTransferChunkSize
= FsConnectionFactory.FILE_TRANSFER_CHUNK_SIZE_DEF_VALUE;
}
if (logger.isLoggable(Level.FINER))
{
logger.finer("Created FsConnection with attributes TempDir " + mTmpDir
+ ", FileTransferChunkSize " + mFileTransferChunkSize);
}
}
/** {@inheritDoc} */
public void close ()
throws ResourceException
{
logger.entering(CLASSNAME, "close");
setClosed();
// be sure that all file access handles are closed
closeAllCloseables();
logger.exiting(CLASSNAME, "close");
}
/** {@inheritDoc} */
public boolean isExists (String file)
throws ResourceException
{
final String method = "isExists";
logger.entering(CLASSNAME, method, file);
assertValid();
final boolean result = isFileExists(new File(file));
logger.exiting(CLASSNAME, method, Boolean.valueOf(result));
return result;
}
static javax.resource.spi.SecurityException
createReadAccessSecurityException (String file,
java.lang.SecurityException se)
{
final javax.resource.spi.SecurityException rse
= new javax.resource.spi.SecurityException(
"The SecurityManager denies read access to the file '"
+ file + "' or to the parent derectory.");
se.initCause(se);
return rse;
}
/** {@inheritDoc} */
public boolean deleteFile (String file)
throws ResourceException
{
assertValid();
return deleteFile(new File(file));
}
/** {@inheritDoc} */
public String [] listFiles (final String dir)
throws ResourceException
{
final String method = "listFiles";
logger.entering(CLASSNAME, method, dir);
assertValid();
final File file = new File(dir);
final String [] result;
try
{
result = file.list();
}
catch (java.lang.SecurityException se)
{
final javax.resource.spi.SecurityException rse
= new javax.resource.spi.SecurityException("The SecurityManager "
+ "denies read access to the directory '" + dir + "'.");
rse.initCause(se);
logger.throwing(CLASSNAME, method, rse);
throw rse;
}
logger.exiting(CLASSNAME, method, result);
return result;
}
/** {@inheritDoc} */
public void renameFile (final String from, final String to)
throws ResourceException
{
final String method = "rename";
logger.entering(CLASSNAME, method, new Object [] {from, to});
assertValid();
moveFile(from, to);
logger.exiting(CLASSNAME, method);
}
/** {@inheritDoc} */
public void moveFile (final String src, final String dest)
throws ResourceException
{
final String method = "move";
logger.entering(CLASSNAME, method, new Object [] {src, dest});
assertValid();
final File srcFile = new File(src);
final File destFile = new File(dest);
ResourceException re = null;
File backupFile = null;
if (!isFileExists(srcFile))
{
re = new ResourceException("File '" + srcFile.toString()
+ "' does not exist.");
logger.throwing(CLASSNAME, method, re);
throw re;
}
if (!srcFile.canWrite())
{
re = new ResourceException("File '" + srcFile.toString()
+ "' is not writable.");
logger.throwing(CLASSNAME, method, re);
throw re;
}
if (isFileExists(destFile))
{
if (!destFile.canWrite())
{
re = new ResourceException("The destination file '"
+ destFile.toString() + "' does already exist and is not "
+ "writable.");
logger.throwing(CLASSNAME, method, re);
throw re;
}
backupFile = getBackupFile(destFile);
re = rename(destFile, backupFile);
if (re != null)
{
logger.throwing(CLASSNAME, method, re);
throw re;
}
}
else
{
createParentDirs(destFile);
}
re = rename(srcFile, destFile);
if (re != null)
{
if (backupFile != null)
{
// rollback
final ResourceException e = rename(backupFile, destFile);
if (e != null)
{
logger.log(Level.WARNING, "Error while file renaming.", e);
}
}
logger.throwing(CLASSNAME, method, re);
throw re;
}
if (backupFile != null && !backupFile.delete())
{
re = new ResourceException("Could not delete file '"
+ backupFile.toString() + "'.");
logger.throwing(CLASSNAME, method, re);
throw re;
}
logger.exiting(CLASSNAME, method);
}
/** {@inheritDoc} */
public String createTempFile ()
throws ResourceException
{
final String method = "createTempFile";
logger.entering(CLASSNAME, method);
assertValid();
final String result = createNewFile(null);
logger.exiting(CLASSNAME, method, result);
return result;
}
/** {@inheritDoc} */
public String createTempFile (final String dir)
throws ResourceException
{
final String method = "createTempFile";
logger.entering(CLASSNAME, method, dir);
assertValid();
final File f = new File(dir);
if (!isFileExists(f) || !f.isDirectory())
{
final ResourceException re = new ResourceException(
"Failed to create a new file. A directory named '" + dir
+ "' does not exist.");
logger.throwing(CLASSNAME, method, re);
throw re;
}
final String result = createNewFile(dir);
logger.exiting(CLASSNAME, method, result);
return result;
}
/** {@inheritDoc} */
public boolean createFile (String file)
throws ResourceException
{
final String method = "createFile";
logger.entering(CLASSNAME, method, file);
assertValid();
final File f = new File(file);
// check if the file does already exist
if (isFileExists(f))
{
final ResourceException re = new ResourceException(
"Failed to create a new file. A file named '" + file
+ "' does already exist.");
logger.throwing(CLASSNAME, method, re);
throw re;
}
// check all necessary parent directories do exist.
createParentDirs(f);
final boolean result;
try
{
result = f.createNewFile();
}
catch (IOException e)
{
final ResourceException re = new ResourceException(
"Failed to create a new file '" + file + "'.");
re.initCause(e);
logger.throwing(CLASSNAME, method, re);
throw re;
}
catch (java.lang.SecurityException se)
{
final javax.resource.spi.SecurityException rse
= new javax.resource.spi.SecurityException(
"Failed to create a new file.");
rse.initCause(se);
logger.throwing(CLASSNAME, method, rse);
throw rse;
}
logger.exiting(CLASSNAME, method, Boolean.valueOf(result));
return result;
}
/**
* @param method
* @throws ResourceException
* @throws SecurityException
*/
private void createParentDirs (final File f)
throws ResourceException, SecurityException
{
final String method = "createParentDirs";
logger.entering(CLASSNAME, method, f);
final File parent = f.getParentFile();
if (!isFileExists(parent))
{
// if not -> creates all missing parent directories
try
{
if (!parent.mkdirs())
{
final ResourceException re = new ResourceException("Failed to "
+ "create all necessary file directories: "
+ parent.toString());
logger.throwing(CLASSNAME, method, re);
throw re;
}
logger.finer("Created all necessary parant directories "
+ parent.toString());
}
catch (java.lang.SecurityException se)
{
final javax.resource.spi.SecurityException rse
= new javax.resource.spi.SecurityException("Failed to create "
+ "all necessary file directories: "
+ parent.toString());
rse.initCause(se);
logger.throwing(CLASSNAME, method, rse);
throw rse;
}
}
logger.exiting(CLASSNAME, method);
}
/** {@inheritDoc} */
public String renameToTempFile (String file)
throws ResourceException
{
final String method = "renameToTempFile";
logger.entering(CLASSNAME, method, file);
assertValid();
final String result;
try
{
final File tempFile = createTempFileInTempDir();
tempFile.delete();
final File srcFile = new File(file);
if (srcFile.renameTo(tempFile))
{
result = tempFile.getAbsolutePath();
}
else
{
final ResourceException re = new ResourceException(
"The rename operation failed.");
logger.throwing(CLASSNAME, method, re);
throw re;
}
}
catch (SecurityException se)
{
final javax.resource.spi.SecurityException rse
= new javax.resource.spi.SecurityException("Security Manager "
+ "does not allow the file '" + file + "' to be renamed or"
+ " a temp file to be created");
rse.initCause(se);
logger.throwing(CLASSNAME, method, rse);
throw rse;
}
catch (IOException ioe)
{
final ResourceException re = new ResourceException("The rename "
+ "operation failed.");
re.initCause(ioe);
logger.throwing(CLASSNAME, method, re);
throw re;
}
logger.exiting(CLASSNAME, method, result);
return result;
}
private File createTempFileInTempDir ()
throws IOException
{
return File.createTempFile(TMP_FILE_PREFIX, TMP_FILE_SUFFIX, mTmpDir);
}
/**
* Creates a new file. If the parameter <code>file</code> is null - creates
* an empty file in the default temporary-file directory; if the parameter
* <code>file</code> denotes to an existing directory - creates an empty file
* in this directory; otherwise creates an empty file named
* <code>file</code>.
*
* @param file null, directory name or file name.
*
* @return The name of the created file.
*
* @throws ResourceException thrown if the file could not be created.
*/
private String createNewFile (final String file)
throws ResourceException
{
final String method = "createNewFile";
logger.entering(CLASSNAME, method, file);
assertValid();
final File resultFile;
if (file == null)
{
resultFile = createTempDir(mTmpDir);
}
else
{
final File f = new File(file);
if (isFileExists(f) && f.isDirectory())
{
resultFile = createTempDir(f);
}
else
{
resultFile = crateNewFile(f);
}
}
final String result = resultFile.toString();
logger.exiting(CLASSNAME, method, result);
return result;
}
/**
* Creates a new file <code>file</code>.
* @param file The file to be created.
* @return The <code>file</code>.
* @throws ResourceException thrown if the file could not be created.
*/
File crateNewFile (final File file)
throws ResourceException
{
final String msg = "Failed to create '" + file.getAbsolutePath() + "'";
try
{
if (!file.createNewFile())
{
final ResourceException re = new ResourceException(
"Failed to create a new file. The file '" + file.toString()
+ "' does already exist.");
throw re;
}
}
catch (IOException e)
{
final ResourceException re = new ResourceException(msg);
re.initCause(e);
throw re;
}
catch (java.lang.SecurityException se)
{
final javax.resource.spi.SecurityException rse
= new javax.resource.spi.SecurityException(msg);
rse.initCause(se);
throw rse;
}
return file;
}
/**
* Creates a new temp file within of the given temp dir.
* @param tempDirFile The temp dir
* @return The new temp file.
* @throws ResourceException thrown if the file could not be created.
*/
File createTempDir (final File tempDirFile)
throws ResourceException, javax.resource.spi.SecurityException
{
final File result;
final String msg = "Failed to create a new temp file, temp dir '"
+ tempDirFile.toString() + "'";
try
{
result = File.createTempFile(TMP_FILE_PREFIX, TMP_FILE_SUFFIX,
tempDirFile);
}
catch (IOException e)
{
final ResourceException re = new ResourceException(msg);
re.initCause(e);
throw re;
}
catch (java.lang.SecurityException se)
{
final javax.resource.spi.SecurityException rse
= new javax.resource.spi.SecurityException(msg);
rse.initCause(se);
throw rse;
}
return result;
}
/**
* Renames file <code>src</code> to <code>dest</code>.
* @param src file to be renamed.
* @param dest the new file name.
* @return ResourceException in case of the rename operation failes.
*/
private ResourceException rename (final File src, final File dest)
{
if (logger.isLoggable(Level.FINER))
{
logger.entering(CLASSNAME, "rename", new Object [] {src, dest});
}
ResourceException re = null;
if (!src.renameTo(dest))
{
// probably the underlying native implementation does not support
// renameTo on different file systems.
try
{
// copy the file
copyFilesChunk(src, dest, mFileTransferChunkSize);
// delete the source file
deleteFile(src);
}
catch (ResourceException r)
{
re = r;
}
}
if (logger.isLoggable(Level.FINER))
{
logger.exiting(CLASSNAME, "rename", re);
}
return re;
}
/**
* @param file The original file.
* @return The File instance that can be used for back up. The File does yet
* not exist.
* @throws SecurityException thown in case of security problem
*/
public static File getBackupFile (final File file)
throws SecurityException
{
File backupFile = getSuggestionForBackupFile(file);
while (isFileExists(backupFile))
{
backupFile = getSuggestionForBackupFile(file);
}
return backupFile;
}
/**
* @param file The original file.
* @return The suggestion for a backup file.
*/
public static File getSuggestionForBackupFile (final File file)
{
return new File(file.getParent(),
file.getName() + Integer.toString(RANDOM.nextInt()));
}
/** {@inheritDoc} */
public RandomAccessFile getRandomAccessFile (String file, String mode)
throws ResourceException, FileNotFoundException
{
final String method = "getRandomAccessFile";
logger.entering(CLASSNAME, method, new Object [] {file, mode});
assertValid();
final RandomAccessFileWrapper raf;
try
{
raf = new RandomAccessFileWrapper(this, file, mode);
}
catch (java.lang.SecurityException se)
{
final javax.resource.spi.SecurityException rse
= createReadAccessSecurityException(file, se);
logger.throwing(CLASSNAME, method, rse);
throw rse;
}
mCloseables.add(raf);
logger.exiting(CLASSNAME, method, raf);
return raf;
}
/** {@inheritDoc} */
public FileInputStream getFileInputStream (String file)
throws ResourceException, FileNotFoundException
{
final String method = "getFileInputStream";
logger.entering(CLASSNAME, method, file);
assertValid();
final FileInputStreamWrapper fis;
try
{
fis = new FileInputStreamWrapper(this, file);
}
catch (java.lang.SecurityException se)
{
final javax.resource.spi.SecurityException rse
= createReadAccessSecurityException(file, se);
logger.throwing(CLASSNAME, method, rse);
throw rse;
}
mCloseables.add(fis);
logger.exiting(CLASSNAME, method, fis);
return fis;
}
/**
* Sets this connection to the state cleaned up. All futher calls on the
* public methods of this connection will throw a ResourceException.
*/
public void cleanUp ()
{
logger.entering(CLASSNAME, "cleanUp");
super.cleanUp();
closeAllCloseables();
logger.exiting(CLASSNAME, "cleanUp");
}
/**
* Closess all RandomAccessFile handles, if yet not closed
*/
private void closeAllCloseables ()
{
logger.entering(CLASSNAME, "closeAllCloseables");
final Iterator itr = mCloseables.iterator();
Closeable c = null;
while (itr.hasNext())
{
c = (Closeable) itr.next();
try
{
c.close();
}
catch (IOException e)
{
logger.log(Level.FINE, "Could not close the Closeable '" + c + "'.",
e);
}
}
mCloseables.clear();
logger.exiting(CLASSNAME, "closeAllCloseables");
}
/**
* Removes the Closeable <code>c</code> from the list of closeable objects.
* @param c Closeable to be removed from the list of closeable objects.
*/
public void closeableClosed (Closeable c)
{
logger.entering(CLASSNAME, "closeableClosed", c);
mCloseables.remove(c);
logger.exiting(CLASSNAME, "closeableClosed");
}
/**
* Deletes the supplied file or directory <code>file</code>.
*
* @param file The file or directory to be deleted.
*
* @return if and only if the file or directory is successfully deleted;
* false otherwise
*
* @throws ResourceException If a security manager does not allow the file to
* be deleted.
*/
static boolean deleteFile (final File file)
throws SecurityException
{
final String method = "deleteFile";
logger.entering(CLASSNAME, method, file);
final boolean result;
try
{
result = file.delete();
}
catch (java.lang.SecurityException se)
{
final javax.resource.spi.SecurityException rse
= new javax.resource.spi.SecurityException(
"The SecurityManager denies delete access to the file '"
+ file.toString() + "'.");
rse.initCause(se);
logger.throwing(CLASSNAME, method, rse);
throw rse;
}
logger.exiting(CLASSNAME, method, Boolean.valueOf(result));
return result;
}
static void copyFilesChunk (File src, File dist, long chunkSize)
throws ResourceException
{
final String method = "copyFiles";
final boolean finer = logger.isLoggable(Level.FINER);
if (finer)
{
logger.entering(CLASSNAME, method, new Object [] {src, dist});
}
FileInputStream fis = null;
FileOutputStream fos = null;
FileChannel srcChannel = null;
FileChannel dstChannel = null;
try
{
// Create input stream on the source
fis = new FileInputStream(src);
// Create output stream on the source
fos = new FileOutputStream(dist);
// Create channel on the source
srcChannel = fis.getChannel();
// Create channel on the destination
dstChannel = fos.getChannel();
// Determine the channel size
final long size = srcChannel.size();
logger.finer("srcChannel.size() " + size);
long chunk = chunkSize < size ? chunkSize : size;
long currentpos = 0;
long chunkTransfered = 1L;
long transfered = 0;
while (chunkTransfered > 0 && transfered != size)
{
// Copy file contents from source to destination
chunkTransfered = dstChannel.transferFrom(srcChannel, currentpos,
chunk);
currentpos = chunk + currentpos;
if (finer)
{
logger.finer("Using chunk " + chunk + "Copied next chunk "
+ chunkTransfered + ", new pos " + currentpos);
}
if (chunk != chunkTransfered)
{
final ResourceException re = new ResourceException(
"Failed to copy file '" + src.toString() + "' to '"
+ dist.toString()
+ "' Not all bytes could be transfered.");
logger.throwing(CLASSNAME, method, re);
throw re;
}
transfered = transfered + chunkTransfered;
if (chunkSize >= size - transfered)
{
chunk = size - transfered;
}
if (finer)
{
logger.finer("Transfered " + transfered + " bytes from " + src
+ " to " + dist + ".");
}
}
}
catch (IOException e)
{
final ResourceException re = new ResourceException(
"Failed to copy file '" + src.toString() + "' to '"
+ dist.toString() + "'.");
re.initCause(e);
logger.throwing(CLASSNAME, method, re);
throw re;
}
finally
{
// Close the channels
IoUtil.close(srcChannel);
IoUtil.close(dstChannel);
// Close the streams
IoUtil.close(fis);
IoUtil.close(fos);
}
logger.exiting(CLASSNAME, method);
}
/** {@inheritDoc} */
public String toString ()
{
return mStringified;
}
/** {@inheritDoc} */
public int hashCode ()
{
return mIndex;
}
/** {@inheritDoc} */
public boolean equals (Object obj)
{
return (obj instanceof FsConnectionImpl
&& ((FsConnectionImpl) obj).mIndex == this.mIndex);
}
static boolean isFileExists (final File file)
throws SecurityException
{
boolean result = false;
try
{
result = file.exists();
if (!result)
{
final File parent = file.getParentFile();
if (parent != null)
{
final File [] lst = parent.listFiles(new SingleFileFilter(file));
if (lst != null && lst.length == 1 && lst[0].equals(file))
{
result = true;
}
}
}
}
catch (java.lang.SecurityException se)
{
final javax.resource.spi.SecurityException rse
= createReadAccessSecurityException(file.toString(), se);
throw rse;
}
return result;
}
private static final class SingleFileFilter
implements FileFilter
{
private final File mFile;
public SingleFileFilter (File org)
{
mFile = org;
}
public boolean accept (File pathname)
{
return mFile.equals(pathname);
}
}
}