package org.cdlib.xtf.util;
/**
* Copyright (c) 2004, Regents of the University of California
* 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 University of California 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 COPYRIGHT HOLDERS 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 COPYRIGHT OWNER OR 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.
*/
import java.io.DataInput;
import java.io.DataOutput;
import java.io.EOFException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.LinkedList;
import java.util.HashMap;
/**
* A simple structured file with a flat top-level directory. Subfiles
* can be added to an existing file.
*
* @author Martin Haye
*/
public class StructuredFile implements StructuredStore
{
/** Actual file path of the structured file */
private File file;
/** Used to read/write the disk file */
private RandomAccessFile realFile;
/**
* File position of the subfile directory (zero if the directory has been
* erased)
*/
private int dirPos;
/** Current subfile directory */
private Directory dir;
/**
* True when creating a sub-file; enforces the rule that only one sub-file
* may be created at a time.
*/
private SubFileWriter creatingSubfile;
/**
* Directory entry of the sub-file being created (if
* {@link #creatingSubfile} is true).
*/
private DirEntry creatingEnt;
/** List of currently opened subfiles */
private LinkedList openSubfiles = new LinkedList();
/**
* The sub-file that last accessed the file. This is checked every time
* a sub-file wants to access the file, to see if the file pointer needs
* to be saved/restored. Package-private, since the Subfile class needs
* access to it.
*/
Object curSubFile = null;
/** Number of currently open Structured files */
private int openCount = 0;
/**
* Map of currently open Structured files. Used to ensure that
* only one instance of a given file is in memory at any given time,
* to avoid concurrency problems.
*/
private static HashMap fileMap = new HashMap();
/**
* Instances should never be created by outside parties, so the constructor
* is strictly private.
*/
private StructuredFile(File file, boolean create)
throws IOException
{
this.file = file;
if (!create && !file.exists())
throw new FileNotFoundException(file.toString());
realFile = new RandomAccessFile(file, "rw");
try
{
if (create)
{
realFile.setLength(0);
// Now write the header. First the identifier.
realFile.writeByte('s');
realFile.writeByte('s');
realFile.writeByte('f');
realFile.writeByte(0);
// Write a placeholder for the directory position.
realFile.writeInt(0);
// Now write a new directory.
dir = new Directory();
writeDirectory();
}
else
readHeader();
}
catch (IOException e) {
if (realFile != null)
realFile.close();
file.delete();
throw e;
}
} // constructor
/** Get the full path to the file */
public String getSystemId() {
return file.getAbsolutePath();
}
/**
* Reads the file header and directory of the structured file. Called by
* constructor.
*/
private void readHeader()
throws IOException
{
realFile.seek(0);
// Verify the header.
if (realFile.readByte() != 's' ||
realFile.readByte() != 's' ||
realFile.readByte() != 'f' ||
realFile.readByte() != 0)
throw new IOException("File is not a structured file");
// Get the position of the directory, and read it in.
dirPos = realFile.readInt();
realFile.seek(dirPos);
dir = new Directory(realFile);
} // readHeader()
/**
* Create a structured file from scratch. Any existing file is replaced by
* a new empty one.
*
* @param file The file path to write to.
*/
public static synchronized StructuredFile create(File file)
throws IOException
{
StructuredFile sf;
// Do we already have an open instance?
if (fileMap.get(file) != null)
sf = (StructuredFile)fileMap.get(file);
else {
sf = new StructuredFile(file, true);
fileMap.put(file, sf);
}
sf.openCount++;
return sf;
}
/**
* Open an existing structured file.
* @param file The file to open.
* @throws FileNotFoundException If the file doesn't exist.
*/
public static synchronized StructuredFile open(File file)
throws FileNotFoundException, IOException
{
StructuredFile sf;
if (fileMap.get(file) != null)
sf = (StructuredFile)fileMap.get(file);
else {
sf = new StructuredFile(file, false);
fileMap.put(file, sf);
}
sf.openCount++;
return sf;
}
/**
* Create a new sub-file with the specified name. Returns a Subfile that
* has most of the interface of a RandomAccessFile, except that seeks
* will be relative to the sub-file start.
*
* Only one subfile may be created at a time (though many others may be
* opened, provided they were created before.)
*
* The caller must call Subfile.close() when the file is complete, to
* ensure that the directory gets written.
*
* @param name Name of the sub-file to create. Must not exist.
* @return A subfile to write to.
*/
public synchronized SubStoreWriter createSubStore(String name)
throws IOException
{
// Can only create one sub-file at a time.
if (creatingSubfile != null)
throw new IOException("Can only create one sub-file at a time");
// Make sure it doesn't exist, since we have no way to overwrite one.
if (dir.find(name) != null)
throw new IOException("Cannot create sub-file: already exists");
// Erase the current directory (it will be overwritten by the new
// subfile).
//
eraseDirectory();
// Add a new entry to the directory (but leave the length at zero.)
DirEntry ent = new DirEntry();
ent.name = name;
ent.segOffset = (int)realFile.length();
dir.add(ent);
// Finally create the Subfile and record that it's open now.
SubFileWriter subfile = new SubFileWriter(realFile, this, ent.segOffset);
creatingSubfile = subfile;
creatingEnt = ent;
return subfile;
} // createSubfile()
/**
* Opens a pre-existing subfile for read (or write). Returns a subfile that
* has most of the interface of a RandomAccessFile, except that seeks will
* be relative to the sub-file start, and IO operations cannot exceed the
* boundaries of the sub-file.
*
* Many subfiles may be open simultaneously; each one has an independent
* file pointer. Each one is light weight, so it's okay to have many open
* at a time.
*
* @param name Name of pre-existing subfile to open.
*/
public synchronized SubStoreReader openSubStore(String name)
throws IOException
{
// Find the directory entry.
DirEntry ent = dir.find(name);
if (ent == null)
throw new FileNotFoundException("Sub-file " + name + " not found.");
// Make sure this isn't currently being created.
if (creatingEnt == ent)
throw new IOException("Cannot open in-progress subfile");
// Make a sub-file instance to read it.
SubFileReader sub = new SubFileReader(realFile,
this,
ent.segOffset,
ent.segLength);
openSubfiles.add(sub);
return sub;
} // openSubfile()
/**
* Called by a subfile reader when its close() method is called.
*
* @param subfile The sub-file reader being closed.
*/
synchronized void closeReader(SubFileReader subfile)
throws IOException
{
// Remove this from the list of open files.
if (!openSubfiles.remove(subfile))
assert false : "Tried to close sub-file not in list";
if (curSubFile == subfile)
curSubFile = null;
} // closeSubfile()
/**
* Called by a subfile when its close() method is called. If the subfile is
* newly created, we update the directory.
*
* @param subfile The sub-file being closed.
*/
synchronized void closeWriter(SubFileWriter subfile)
throws IOException
{
// If this is a newly created file, update the directory entry.
if (creatingSubfile == subfile) {
creatingEnt.segLength = (int)realFile.length() - creatingEnt.segOffset;
writeDirectory();
creatingSubfile = null;
creatingEnt = null;
}
else
assert false : "Tried to close unknown sub-file writer";
} // closeSubfile()
/**
* Sets a user-defined version number for the store. It can be retrieved
* later with {@link #getUserVersion()}.
*
* @param ver The version number to set.
*/
public synchronized void setUserVersion(String ver)
throws IOException
{
// Only update if changing.
if (dir.getUserVersion().equals(ver))
return;
// Okay, change the directory.
dir.setUserVersion(ver);
// Write the new directory on disk, unless it will already be when
// a created file is closed.
//
if (dirPos != 0) {
eraseDirectory();
writeDirectory();
}
} // setUserVersion()
/**
* Gets the user version (if any) set by {@link #setUserVersion(String)}.
*/
public String getUserVersion() {
return dir.getUserVersion();
} // getUserVersion()
/**
* Closes the file. This should always be called, to ensure that all
* sub-files have been closed and that the directory has been written.
*/
public synchronized void close()
throws IOException
{
synchronized (getClass())
{
// Decrement the count, and if it's not zero yet, wait for another
// close later on.
//
openCount--;
if (openCount > 0)
return;
// Remove this instance from the file map, so it never gets used again.
fileMap.remove(file);
// If a file was being created, close it.
if (creatingSubfile != null) {
creatingSubfile.close();
creatingSubfile = null;
}
// If any subfiles are open, close them too.
while (!openSubfiles.isEmpty()) {
SubFileReader sub = (SubFileReader)openSubfiles.getFirst();
sub.close();
} // while
// And close the underlying file.
if (realFile != null) {
realFile.close();
realFile = null;
}
}
} // close()
/** Close and delete the entire structured file */
public void delete()
throws IOException
{
// Close if we haven't already.
if (realFile != null)
close();
// And delete the file.
file.delete();
} // delete()
/**
* Gets rid of the directory at the end of the file, in preparation for
* overwriting it with a new subfile. When the subfile is complete, call
* writeDirectory() to record the change.
*/
private void eraseDirectory()
throws IOException
{
// If already erased, don't do anything.
if (dirPos == 0)
return;
// Truncate the file (erasing the old directory)
realFile.setLength(dirPos);
// Change the pointer at the start of the file to zero, denoting that
// the directory has been erased.
//
realFile.seek(4);
realFile.writeInt(0);
dirPos = 0;
} // eraseDirectory()
/**
* Writes the current directory at the end of the file. Assumes the
* previous directory was already erased (by calling eraseDirectory()).
*/
private void writeDirectory()
throws IOException
{
if (dirPos != 0)
throw new IOException("Cannot writeDirectory() before eraseDirectory()");
// The new directory position: the end of the file.
dirPos = (int)realFile.length();
// Write it out.
realFile.seek(dirPos);
dir.writeTo(realFile);
// And update the directory position pointer at the start of the file.
realFile.seek(4);
realFile.writeInt(dirPos);
} // writeDirectory()
/**
* Maintains the directory of files within a structured file.
*/
private class Directory
{
/** All the entries currently in the directory */
private DirEntry[] entries;
/** Version string established by the client */
private String userVersion;
/** Create a new directory */
public Directory() {
entries = new DirEntry[0];
userVersion = "";
}
/** Read a directory from a DataInput stream */
public Directory(DataInput in)
throws IOException
{
// Read the magic code.
if (in.readByte() != 'd' ||
in.readByte() != 'i' ||
in.readByte() != 'r' ||
in.readByte() != 0)
throw new IOException("Structured file directory corrupted");
// Read the length in bytes, then fetch all the data.
int length = in.readInt();
PackedByteBuf buf = new PackedByteBuf(in, length);
// Read the user version.
userVersion = buf.readString();
// Find out how many entries there are, then read them all in.
int nEntries = buf.readInt();
entries = new DirEntry[nEntries];
for (int i = 0; i < nEntries; i++)
entries[i] = new DirEntry(buf);
} // constructor
/** Write out the current directory to a DataOutput stream */
public void writeTo(DataOutput out)
throws IOException
{
// Pack up the user version.
PackedByteBuf buf = new PackedByteBuf(500);
buf.writeString(userVersion);
// Pack up all the entries.
buf.writeInt(entries.length);
for (int i = 0; i < entries.length; i++)
entries[i].writeTo(buf);
// Write the magic code.
out.writeByte('d');
out.writeByte('i');
out.writeByte('r');
out.writeByte(0);
// Write the length (in bytes) of the packed entries, then the
// entries themselves.
//
out.writeInt(buf.length());
buf.output(out);
} // writeTo()
/**
* Locate the named directory entry.
*
* @param name is the sub-file name to look for
* @return the directory entry, or null if not found.
*/
public DirEntry find(String name)
{
for (int i = 0; i < entries.length; i++) {
if (entries[i].name.equals(name))
return entries[i];
}
return null;
}
/** Add an entry to the Directory */
public void add(DirEntry entry) {
DirEntry[] newEntries = new DirEntry[entries.length + 1];
System.arraycopy(entries, 0, newEntries, 0, entries.length);
newEntries[entries.length] = entry;
entries = newEntries;
}
/**
* @return Returns the user version.
*/
public String getUserVersion() {
return userVersion;
}
/**
* @param userVersion The user-defined version number to set.
*/
public void setUserVersion(String userVersion) {
this.userVersion = userVersion;
}
} // class Directory
/** A single entry in a Directory */
private class DirEntry
{
/** Sub-file name */
public String name;
/** Absolute file offset of the sub-file's start */
public int segOffset;
/** Length of the sub-file */
public int segLength;
/** Create an empty directory entry */
public DirEntry() {
}
/** Read a directory entry from a PackedByteBuf */
public DirEntry(PackedByteBuf buf) {
name = buf.readString();
segOffset = buf.readInt();
segLength = buf.readInt();
} // constructor
/** Write a directory entry to a PackedByteBuf */
public void writeTo(PackedByteBuf buf) {
buf.writeString(name);
buf.writeInt(segOffset);
buf.writeInt(segLength);
} // writeTo()
} // class DirEntry
/**
* Regression test to make sure the code works properly. Creates a file in
* the current directory, then erases it.
*/
public static final Tester tester = new Tester("StructuredFile")
{
protected void testImpl()
throws Exception
{
File testFile = new File("test.sf");
StructuredFile f = null;
try
{
// Create a file.
f = StructuredFile.create(testFile);
f.close();
f = StructuredFile.open(testFile);
// Add a couple sub-files.
SubStoreWriter sfw1 = f.createSubStore("foo");
sfw1.writeInt(1);
sfw1.writeByte(2);
sfw1.writeInt(3);
sfw1.close();
SubStoreWriter sfw2 = f.createSubStore("foo2");
sfw2.writeByte(8);
sfw2.writeInt(9);
sfw2.close();
f.close();
// Verify the sub-files.
f = StructuredFile.open(testFile);
SubStoreReader sfr2 = f.openSubStore("foo2");
SubStoreReader sfr1 = f.openSubStore("foo");
assert sfr2.readByte() == 8;
assert sfr1.readInt() == 1;
assert sfr1.readByte() == 2;
assert sfr2.readInt() == 9;
assert sfr1.readInt() == 3;
// Make sure we can't read past the end of either one.
boolean ok = false;
try {
sfr1.readInt();
}
catch (EOFException e) {
ok = true;
}
assert ok;
ok = false;
try {
sfr2.readByte();
}
catch (EOFException e) {
ok = true;
}
assert ok;
// Test seeking.
sfr1.seek(4);
assert sfr1.readByte() == 2;
sfr1.seek(0);
assert sfr1.readInt() == 1;
ok = false;
try {
sfr1.seek(20);
}
catch (IOException e) {
ok = true;
}
assert ok;
// Make sure we can add another one.
SubStoreWriter sfw3 = f.createSubStore("foo3");
sfw3.writeInt(10);
// Can't create two at the same time
ok = false;
try {
f.createSubStore("foo4");
}
catch (IOException e) {
ok = true;
}
assert ok;
sfw3.close();
SubStoreReader sfr3 = f.openSubStore("foo3");
assert sfr3.readInt() == 10;
// Shouldn't be able to open a non-existent sub-file.
ok = false;
try {
f.openSubStore("foo99");
}
catch (FileNotFoundException e) {
ok = true;
}
assert ok;
}
finally {
// All done. Close and clean up our file.
if (f != null)
f.close();
testFile.delete();
}
} // testImpl()
};
} // class StructuredFile