/* XXL: The eXtensible and fleXible Library for data processing
Copyright (C) 2000-2011 Prof. Dr. Bernhard Seeger
Head of the Database Research Group
Department of Mathematics and Computer Science
University of Marburg
Germany
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; If not, see <http://www.gnu.org/licenses/>.
http://code.google.com/p/xxl/
*/
package xxl.core.io.fat;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.List;
import xxl.core.io.fat.errors.DirectoryException;
import xxl.core.io.fat.errors.FileDoesntExist;
import xxl.core.io.fat.errors.InitializationException;
import xxl.core.io.fat.util.ByteArrayConversionsLittleEndian;
import xxl.core.io.fat.util.MyMath;
/**
* This class has all operations that RandomAccessFile has, but uses our
* device for this operations. If you order an RandomAccessFile from the class
* FATDevice you get this class back and you can use it as a RandomAccessFile.
* Some information about how the file works: If you create a new file that
* doesn't exist the file size will be zero and no cluster is allocated
* to store data. Only if you use a write method or the method setLength(newLength)
* clusters are allocated to store data. The constructor checks if there is already
* a file with the given fileName this causes some IO-Operations. If you change the
* file length either by a write or setLength(newLength) method the file length
* stored at the directory entry in the directory will not change. The new file
* length will be stored at this directory entry only if you close this file
* by the close method. This is done because otherwise there are to much IO-
* Operations to actualize the file length. To minimize the IO-Operations
* further this class manage a filePointer, which points to the actual position
* (all read/write operations are done at this position), a cluster number, and
* a sector number they point to the actual cluster and sector. From time to time
* the first and the last cluster number of the file is needed therefore it manages
* this numbers also.
* If an instance of this object is created the first call has to be the
* super constructor this needs a fileName and will open that file. Therefore we
* use a dummy file 'dummyFile' with read only access that will be closed after
* the super constructor has been called.
*/
public class ExtendedRandomAccessFile extends RandomAccessFile
{
/**
* The whole functionality of the methods is directed to device.
*/
private FATDevice device = null;
/**
* The name of the file.
*/
private String fileName;
/**
* The mode of the file it is either "r" which means read access only or
* it is "rw" which means read and write access.
*/
private String mode;
/**
* The file pointer is an index to the actual position within the file.
* All read or write accesses occur at the file pointer position.
*/
private long filePointer;
/**
* The number of the actual cluster where the filePointer points to.
*/
private long clusterNumber;
/**
* The start cluster number of the file. As long as it is zero it's not valid.
*/
private long startClusterNumber = 0;
/**
* The last cluster number of the file.
*/
private long lastClusterNumber = 0;
/**
* The number of the actual sector number where the filePointer points to.
*/
private long sectorNumber;
/**
* Counter that determines how many sectors within a cluster has been read/written.
*/
private long sectorCounter;
/**
* Flag that indicates if the clusterNumber points to the EOC_MARK.
*/
private boolean isLastCluster;
/**
* The actual length of the file.
*/
private long fileLength;
/**
* Contains the length of the file before a file length changing operation.
*/
private long oldFileLength;
/**
* The number of sectors per cluster.
*/
private int secPerClus;
/**
* The number of bytes per sector.
*/
private int bytsPerSec;
/**
* Buffered sector
*/
private byte bufferedSector[];
/**
* Indicates if the buffered sector has changed
*/
private boolean bufferedSectorChanged;
/**
* Buffered sector.
*/
private long sectorInBuffer;
/**
* Creates a random access file stream to read from, and optionally
* to write to, the file specified by the File argument. A new
* FileDescriptor object is created to represent this file connection.
*
* The mode argument must either be equal to "r" or "rw", indicating
* that the file is to be opened for input only or for both input and
* output, respectively. The write methods on this object will always
* throw an IOException if the file is opened with a mode of "r". If
* the mode is "rw" and the file does not exist, then an attempt is
* made to create it. An IOException is thrown if the file argument
* refers to a directory.
*
* If there is a security manager, its checkRead method is called with
* the pathname of the file argument as its argument to see if read
* access to the file is allowed. If the mode is "rw", the security
* manager's checkWrite method is also called with the path argument
* to see if write access to the file is allowed.
*
* @param device object of FATDevice.
* @param file the file object.
* @param mode the access mode.
* @param dummyFile The implementation of the base class needs a file
* that exists inside the file system. The file is never used,
* just opened as read only and immediately closed again. This parameter
* only exists because of the inflexible implementation of RandomAccessFile.
* @throws IllegalArgumentException if the mode argument is not equal
* to "r" or to "rw".
* @throws FileNotFoundException if the file exists but is a directory
* rather than a regular file, or cannot be opened or created for any other reason.
* @throws DirectoryException in case of a directory error.
*/
protected ExtendedRandomAccessFile(FATDevice device, ExtendedFile file, String mode, File dummyFile) throws FileNotFoundException, DirectoryException
{
this(device, file.getAbsolutePath(), mode, dummyFile);
} //end constructor
/**
* Construct an instance of this object.
* @param device an object of the device where this file works on.
* @param fileName the name of the file.
* @param mode the mode of the file is either "r" which means read access only or
* it is "rw" which means read and write access.
* @param dummyFile The implementation of the base class needs a file
* that exists inside the file system. The file is never used,
* just opened as read only and immediately closed again. This parameter
* only exists because of the inflexible implementation of RandomAccessFile.
* @throws FileNotFoundException in case the file is read-only but doesn't exist.
* @throws IllegalArgumentException in case one or more arguments are illegal.
* @throws DirectoryException in case of a directory error.
*/
protected ExtendedRandomAccessFile(FATDevice device, String fileName, String mode, File dummyFile) throws FileNotFoundException, DirectoryException {
//if we use RandomAccessFile we have to create a file
//therefore we use a dummy file with read access only
//but we will never use this file, all file activities
//will be directed to our device with the correct
//file given by fileName and the correct mode
//In later versions we can use a ram-disk for the file, so
//that no file operation on disk is used.
super(dummyFile, "r");
try
{
//we never use this file -> close it
super.close();
}
catch(Exception e)
{
throw new InitializationException("Couldn't initialize ExtendedRandomAccessFile, because of: "+e);
}
if (!mode.equals(FATDevice.FILE_MODE_READ) &&
!mode.equals(FATDevice.FILE_MODE_READ_WRITE))
throw new IllegalArgumentException("The mode has to be either FATDevice.FILE_MODE_READ or FATDevice.FILE_MODE_READ_WRITE!");
if (device.isDirectory(fileName))
throw new FileNotFoundException("A directory is not accessible as an ExtendedRandomAccessFile!");
this.device = device;
this.fileName = fileName;
this.mode = mode;
try {
if (device.fileExists(fileName)) {
//get the cluster number of the directory entry indicated by fileName
clusterNumber = device.getStartClusterNumber(fileName);
startClusterNumber = clusterNumber;
//calculate the start sector number from the clusterNumber
sectorNumber = device.getFirstSectorNumberOfCluster(clusterNumber);
isLastCluster = device.isLastCluster(clusterNumber);
fileLength = device.length(fileName);
}
else if (mode.equals(FATDevice.FILE_MODE_READ_WRITE)) {
clusterNumber = device.createFile(fileName);
//at this moment the clusterNumber points to an incorrect value
isLastCluster = true; //there is no reserved cluster at this moment
fileLength = 0;
}
else
throw new FileNotFoundException("You can't read a file that doesn't exist! "+fileName);
lastClusterNumber = getLastClusterNumber();
bytsPerSec = device.getBytsPerSec();
secPerClus = device.getSecPerClus();
}
catch (FileDoesntExist fde) //transform the FileDoesntExist exception to FileNotFoundException
{
throw new FileNotFoundException("Couldn't find the file: "+fileName);
}
filePointer = -1;
sectorCounter = 0;
oldFileLength = fileLength;
sectorInBuffer = -1;
bufferedSectorChanged = false;
bufferedSector = new byte[bytsPerSec];
} //end constructor
/**
* Return the name of this file.
* @return the name of this file
*/
public String getName() {
return fileName;
} //end getName()
/**
* Close this extended random access file stream and release any system
* resources associated with the stream.
* @throws IOException in case of an I/O error.
*/
public void close() throws IOException {
commit();
if (oldFileLength != fileLength)
{
device.writeLength(fileName, fileLength, true);
}
device.close(fileName);
} //end close
/**
* Committing changes of the buffered sector to the device.
*/
private void commit() {
if (bufferedSectorChanged) {
device.writeSector(bufferedSector, sectorInBuffer);
bufferedSectorChanged = false;
}
}
/**
* Read a sector from the device into the buffer. If necessary
* first write back changes.
*
* @param sectorNumber the number of the sector to be read into the buffer.
*/
private void readSectorIntoBuffer(long sectorNumber) {
if (sectorInBuffer==-1) {
device.readSector(bufferedSector, sectorNumber);
sectorInBuffer = sectorNumber;
}
if (sectorInBuffer!=sectorNumber) {
commit();
device.readSector(bufferedSector, sectorNumber);
sectorInBuffer = sectorNumber;
}
}
/**
* Reads a byte of data from this file. The byte is returned as
* an integer in the range 0 to 255 (0x00-0x0ff).
* @return the next byte of data, or -1 if the end of the
* file has been reached.
* @throws IOException if the end of file is exceed.
*/
public int read() throws IOException {
filePointer++; //set filePointer to the actual position
if (filePointer >= fileLength)
return -1;
// throw new IOException("End of file "+fileName+" exceed.");
if (filePointer != 0 && filePointer % (bytsPerSec) == 0)
{
sectorNumber++;
sectorCounter++;
if (sectorCounter == secPerClus)
{
sectorCounter = 0;
if (isLastCluster)
throw new IOException("End of file "+fileName+" exceed.");
clusterNumber = device.getFatContent(clusterNumber); //update clusterNumber
sectorNumber = device.getFirstSectorNumberOfCluster(clusterNumber); //update sectorNumber
isLastCluster = device.isLastCluster(clusterNumber);
}
}
readSectorIntoBuffer(sectorNumber);
return ByteArrayConversionsLittleEndian.convShort(bufferedSector[(int) (filePointer%bytsPerSec)]);
// old: device.readByte(filePointer, sectorNumber);
} //end read()
/**
* Reads up to length bytes of data from this file into an array of bytes.
* @param buffer the buffer into which the data is read.
* @param offset the start offset of the data.
* @param length the maximum number of bytes read.
* @return the total number of bytes read into the buffer, or -1 if
* there is no more data because the end of the file has been reached.
* @throws IOException if an I/O error occurs.
*/
public int read(byte[] buffer, int offset, int length) throws IOException {
if (filePointer + 1 >= fileLength)
return -1;
for (int i=0; i < length; i++)
{
if (filePointer + 1 < fileLength)
buffer[offset + i] = (byte)read();
else
return i;
}
return length;
} //end read(byte[], int, int)
/**
* Reads up to b.length bytes of data from this file into an array of bytes.
* @param buffer the buffer into which the data is read.
* @return the total number of bytes read into the buffer, or -1 if there is
* no more data because the end of this file has been reached.
* @throws IOException - if an I/O error occurs.
*/
public int read(byte[] buffer) throws IOException {
if (filePointer + 1 >= fileLength)
return -1;
for (int i=0; i < buffer.length; i++)
{
if (filePointer + 1 < fileLength)
buffer[i] = (byte)read();
else
return i;
}
return buffer.length;
} //end read(byte[])
/**
* Attempts to skip over n bytes of input discarding the skipped bytes.
* This method may skip over some smaller number of bytes, possibly zero.
* This may result from any of a number of conditions; reaching end of
* file before n bytes have been skipped is only one possibility. This
* method never throws an EOFException. The actual number of bytes
* skipped is returned. If n is negative, no bytes are skipped.
* @param n the number of bytes to be skipped.
* @return the actual number of bytes skipped.
* @throws IOException - if an I/O error occurs.
*/
public int skipBytes(int n) throws IOException {
if (n <= 0)
return -1;
long pos = getFilePointer();
long newPos = pos + n;
if (newPos > fileLength)
newPos = fileLength;
seek(newPos);
return (int)(newPos - pos);
} //end skipBytes(int)
/**
* Writes the specified byte to this file. The write starts at
* the current file pointer.
* @param value the byte to be written.
* @throws IOException - if an I/O error occurs.
*/
public void write(int value) throws IOException {
if (mode.equals(FATDevice.FILE_MODE_READ))
throw new IOException("You have no write access to the file: "+fileName);
//the user seed beyond the end of the file, now
//we need to extend the file size
if (filePointer+1 >= fileLength)
setLength(filePointer + 2);
filePointer++;
if (startClusterNumber == 0) {
clusterNumber = lastClusterNumber;
startClusterNumber = clusterNumber;
sectorNumber = device.getFirstSectorNumberOfCluster(clusterNumber);
}
if (filePointer != 0 && filePointer % (bytsPerSec) == 0) {
// System.out.println("sectorNumber= "+sectorNumber+", sectorCounter="+sectorCounter);
sectorNumber++;
sectorCounter++;
// System.out.println("sectorNumber= "+sectorNumber+", sectorCounter="+sectorCounter);
if (sectorCounter == secPerClus) {
// System.out.println("and further "+clusterNumber);
sectorCounter = 0;
clusterNumber = device.getFatContent(clusterNumber);
sectorNumber = device.getFirstSectorNumberOfCluster(clusterNumber);
isLastCluster = device.isLastCluster(clusterNumber);
}
}
// System.out.print("write("+filePointer+","+value+","+sectorNumber+");");
readSectorIntoBuffer(sectorNumber);
bufferedSector[(int) (filePointer%bytsPerSec)] = (byte) value;
bufferedSectorChanged = true;
// old: device.writeByte(fileName, value, filePointer, sectorNumber);
} //end writeByte(int)
/**
* Writes b.length bytes from the specified byte array to this
* file, starting at the current file pointer.
* @param b the data.
* @throws IOException - if an I/O error occurs.
*/
public void write(byte[] b) throws IOException {
for (int i=0; i < b.length; i++)
writeByte(b[i]);
} //end write(byte[])
/**
* Writes length bytes to this file from the specified byte array starting at
* offset. The user has to be sure that the length
* of the array will not be exceed.
* @param b the data.
* @param offset the start offset in the data.
* @param length the number of bytes to write.
* @throws IOException - if an I/O error occurs.
*/
public void write(byte[] b, int offset, int length) throws IOException {
for (int i=0; i < length; i++)
writeByte(b[offset + i]);
} //end write(byte[], int, int)
/**
* Sets the file-pointer offset, measured from the beginning
* of this file, at which the next read or write occurs. The
* offset may be set beyond the end of the file. Setting the
* offset beyond the end of the file does not change the file
* length. The file length will change only by writing after
* the offset has been set beyond the end of the file.
* @param pos - the offset position, measured in bytes from
* the beginning of the file, at which to set the file pointer.
* @throws IOException if an I/O error occurs.
*/
public void seek(long pos) throws IOException {
if (pos < 0)
return;
pos--;
if (pos < 0)
{
filePointer = pos;
clusterNumber = startClusterNumber;
sectorCounter = 0;
sectorNumber = device.getFirstSectorNumberOfCluster(startClusterNumber);
return;
}
long clustersToSkip = pos / (bytsPerSec*secPerClus);
long sectorsToSkip = (pos - clustersToSkip*bytsPerSec*secPerClus) / bytsPerSec;
//get the cluster number of the directory entry indicated by fileName
if (startClusterNumber > 0)
clusterNumber = startClusterNumber;
else
{
clusterNumber = device.getStartClusterNumber(fileName);
startClusterNumber = clusterNumber;
}
if (clusterNumber != 0)
{
for (int i=0; i < clustersToSkip; i++)
{
//The filePointer is maybe set beyond the end of the file.
//If the user use read an exception will be thrown but if
//the user use write the file length will be extended.
long fatContent = device.getFatContent(clusterNumber);
if (device.isLastCluster(fatContent))
{
lastClusterNumber = clusterNumber;
break;
}
clusterNumber = fatContent;
}
sectorNumber = device.getFirstSectorNumberOfCluster(clusterNumber) + sectorsToSkip;
sectorCounter = sectorsToSkip;
}
filePointer = pos;
} //end seek(pos)
/**
* Returns the current offset in this file.
* @return the offset from the beginning of the file, in bytes,
* at which the next read or write occurs.
* @throws IOException if an I/O error occurs.
*/
public long getFilePointer() throws IOException {
return filePointer + 1;
} //end getFilePointer()
/**
* Returns the length of this file.
* @return the length of this file, measured in bytes.
* @throws IOException if an I/O error occurs.
*/
public long length() throws IOException {
return fileLength;
} //end length()
/**
* Sets the length of this file.
* If the present length of the file as returned by the length
* method is greater than the newLength argument then the file
* will be truncated. In this case, if the file offset as returned
* by the getFilePointer method is greater then newLength then
* after this method returns the offset will be equal to newLength.
* If the present length of the file as returned by the length
* method is smaller than the newLength argument then the file will
* be extended. In this case, the contents of the extended portion
* of the file are not defined.
* @param newLength - The desired length of the file.
* @throws IOException if an I/O error occurs.
*/
public void setLength(long newLength) throws IOException {
long numOfClustersFileLength = MyMath.roundUp((float)fileLength / (float)(secPerClus*bytsPerSec));
long numOfClustersNewLength = MyMath.roundUp((float)newLength / (float)(secPerClus*bytsPerSec));
long diff = numOfClustersFileLength - numOfClustersNewLength;
// System.out.println("diff= "+diff);
//truncate the file if the new length is smaller than the actual file length
if (diff>0) {
//free clusters
//calculate the new last cluster number, that is the cluster number where newLength points to
lastClusterNumber = startClusterNumber;
for (long i=1; i < numOfClustersNewLength; i++)
lastClusterNumber = device.getFatContent(lastClusterNumber);
device.addFreeClustersMarkFirstAsEOC(lastClusterNumber);
seek(newLength);
fileLength = newLength;
}
else if (diff<0) {
//extend the file if the fileLength is smaller than the wished length
List freeClusters;
//order new space
// System.out.println("lcn="+lastClusterNumber+", noc="+numOfClusters);
if (lastClusterNumber == 0) {
freeClusters = device.extendFileSize(fileName, -diff);
clusterNumber = device.getStartClusterNumber(fileName);
startClusterNumber = clusterNumber;
sectorNumber = device.getFirstSectorNumberOfCluster(clusterNumber); //update sectorNumber
}
else
freeClusters = device.extendFileSize(-diff, lastClusterNumber);
//set the new last cluster number
lastClusterNumber = ((Long)freeClusters.get(freeClusters.size() - 1)).longValue();
isLastCluster = device.isLastCluster(clusterNumber);
}
if (fileLength<newLength) {
long saveFileLength = fileLength;
long saveFP = getFilePointer();
fileLength = newLength;
seek(saveFileLength);
for (long i = saveFileLength; i<fileLength; i++)
write(0);
seek(saveFP);
}
else
fileLength = newLength;
if (fileLength<filePointer+1)
seek(fileLength);
} //end setLength(newLength)
/**
* Return the last cluster number that is not the EOC_MARK.
* In case there is no cluster allocated to the file zero is
* returned.
* @return the last cluster number that is not EOC_MARK or zero
* in case no cluster is allocated to the file.
*/
private long getLastClusterNumber() {
if (fileLength == 0 || clusterNumber == 0)
return 0;
long clusterNumberTmp = clusterNumber;
long fatContent = device.getFatContent(clusterNumberTmp);
while (!device.isLastCluster(fatContent)) {
clusterNumberTmp = fatContent;
fatContent = device.getFatContent(clusterNumberTmp);
}
return clusterNumberTmp;
} //end getLastClusterNumber()
/**
* Return the mode of the file, that is either "r" (FATDevice.FILE_MODE_READ) or
* "rw" (FATDevice.FILE_MODE_READ_WRITE).
* @return the mode of the file "r" or "rw".
*/
public String getMode() {
return mode;
} //end getMode()
/**
* Set the file name to the given name.
* @param newName the new file name.
*/
protected void setName(String newName) {
fileName = newName;
} //end setName(String newName)
} //end class ExtendedRandomAccessFile