package java.io;
import lejos.nxt.Flash;
/*
* DEVELOPER NOTES:
* - Requirement is for all files to be contiguous (unfragmented) so
* that they can be executed.
* - Files are stored in the flash memory one file after another.
* - If a user opens a file and wants to write to it, the file system
* shuffles the file to the end so that it has open space to write.
* - This File system is currently unthreaded, so all functions are
* blocked until each call completes. If someone starts writing to
* a file and it needs to be shuffled, a significant pause can
* occur to shuffle files around. If this was threaded it might be
* possible to avoid this pause.
*
* 3/3/2009: TODO Here are some improvements I'm considering:
*
* 1) Now that we have a garbage collector, the fixed sizes of arrays for names (MAX_FILENAME)
* and number of files in system (MAX_FILES) shouldn't be limited. Also, some of the code to
* reuse objects is a little crazy because of no GC.
* 2) It would be nice to thread some of these operations so they could return immediately
* and continue running in background. Would need to synchronize critical operations on the
* same object, and make sure thread operations are kept non-daemon so they complete before
* JVM exits (we don't want a partial write to occur because the JVM terminates).
* 3) Directories is a pretty standard feature of a file system. Useful if we want to
* assign a specific directory to dump our NXT on-board applications (future goal).
* 4) Instead of mirroring the file table data in memory via the files array and totalFiles
* variables, I'd rather read this data live from flash memory every time it is used within
* a method (i.e. keep no persistent variables of file table). That might save memory and prevent
* possible bugs where local data becomes unsynchronized from flash memory file table.
* However, cached operations might become suspect if a program tries to read file information
* before it is done writing. Might not be a great idea actually.
* 5) If we got really ambitious, allow > 1 file open for writing at a time. It would be very
* difficult to implement given our contiguous file requirement.
* 6) Implement the J2ME solution for writing persistent data, to coexist alongside this class.
* The File class might use some methods in that solution.
*
*/
/**
* Implements a file system using pages of flash memory.
* Currently has limited functionality and only supports
* one file open at a time.
*
* @author bb
*/
public class File {
// CONSTANTS:
/**
* MS-DOS File attribute constants:
*/
private static final byte READ_ONLY_ATTR = 0x01;
private static final byte HIDDEN_ATTR = 0x02;
//private static final byte SYSTEM_ATTR = 0x04; // System file
//private static final byte VOLUME_LABEL_ATTR = 0x08;
//private static final byte DIRECTORY_ATTR = 0x10;
//private static final byte ARCHIVE_ATTR = 0x20;
/**
* Number of files the file system can store.
* Defines the size of the files array. If leJOS gets a garbage
* collector we can get rid of this limitation.
*/
public static final byte MAX_FILES = 30;
/**
* Maximum size of file name. Used because no garbage collector.
* If leJOS gets a garbage collector we can recode this. This value
* is used to define the character array charBuff[] below.
*/
private static final byte MAX_FILENAME = 30;
/**
* Signature written to the front of the file table to indicate if the
* flash memory contains file table information. By changing this
* version number/string, the users file system will reformat automatically.
* (i.e. Restarting file system and erasing their current stored classes)
*/
private static final String TABLE_ID = "V_0.4";
/**
* Indicates the starting page of the file table.
*/
private static byte TABLE_START_PAGE = 1;
/**
* Number of pages reserved for storing file table information.
* If we want to allow more files to be stored in system, increase
* this number.
*/
// TODO: To make this expand automatically when necessary, we could start
// with the last page in memory and work backwards. File table would be at
// the end of flash memory instead of start.
private static byte FILE_TABLE_PAGES = 2;
/**
* First page for storing *file data*.
*/
private static byte FILE_START_PAGE = (byte)(TABLE_START_PAGE + FILE_TABLE_PAGES);
/**
* The position (order of bytes) where the number of files
* is stored in the table.
*/
private static byte NUM_FILES_POS = (byte)TABLE_ID.length();
// GLOBAL STATIC CLASS VARIABLES:
/**
* Shared buffer. Using this as static class variable because leJOS
* lacks a garbage collector.
*/
private static byte [] buff = new byte[Flash.BYTES_PER_PAGE];
/**
* Array containing all the Files in the directory.
*/
static File [] files = null;
/**
* The total number of files in the file system. A negative value
* indicates this variable has not been initialized. Using byte, but
* if we expand past the 30 limit (garbage collector) we can use short.
*/
public static byte totalFiles = -1;
/**
* Temp buffer of characters used to read file names. If leJOS gets a
* garbage collector we can eliminate this. Used in readTable()
*/
private static char [] charBuff = new char[MAX_FILENAME];
// INSTANCE VARIABLES (file name, page location of file, file size, exists):
/**
* The name of the file. Initialized in File constructor.
*/
private String file_name;
/**
* The starting page location of this file. All files start at the 0 byte
* position of the page that they start at.
* Init to -1 to indicate not initialized or does that waste memory?
*/
short page_location = -1; // !! Make protected when done tests?
/**
* The length, in bytes, of this file according to the file table.
* A file that does not exists is supposed to equal 0. i.e. The Java SDK
* says that it doesn't get written to the file table until it has bytes.
*/
int file_length; // 0 when not created yet
/**
* Indicates if the file exists as an entry in the file table.
*/
boolean exists = false;
/**
* Byte that stores bit-wise data of file attributes, like hidden,
* locked, compressed, delete on exit, etc...
* See file attribute constants above
*/
byte file_attributes;
/**
* Creates a new File object. If this file exists on disk it will
* represent that file. If the file does not exist, you will need to
* use createNewFile() before writing to the file.
* @param name
*/
public File(String name) {
this(name, true);
}
/**
* A private constructor with the option to check if the file_name already
* exists against the files in the file table. Needed this method because
* the readTable() method created an array of new File objects (hence had
* to call the constructor) but the file list wasn't ready yet so it made
* no sense to check the list.
* @param name File name
* @param checkExists If true, checks filename against list of files.
*/
private File(String name, boolean checkExists) {
if(!File.tableExists()) File.format();
this.file_name = name;
if(files == null) {
files = new File[MAX_FILES];
readTable(files); // Update file data
}
// Check through file system to see if file with same name exists.
if(checkExists) {
for(byte i=0;i<File.totalFiles;i++) {
if(files[i].file_name.equals(this.file_name)) {
this.file_length = files[i].file_length;
this.page_location = files[i].page_location;
this.exists = true;
files[i] = this; // Substitute this object in actual array so it remains synchronized.
}
}
} else
this.exists = true; // If not checking if it exists, means this was made from readTable, therefore it exists for sure.
}
/**
* Deletes the file represented by this File object.
* @return true if the file is successfully deleted; false otherwise
*/
public boolean delete() {
if(!exists()) return false; // Check if file is in file table.
// 1. Find where this object is in the files array:
byte index = -1;
for(byte i=0;i<File.totalFiles;i++) {
if(files[i].file_name.equals(this.file_name)) index = i;
}
// 2. Update File.totalFiles:
--File.totalFiles; // One less file
// 3. If any files remain after this, shuffle them down.
if(files[index + 1] != null) { // Make sure there are files left after this in array
// Shuffle array File objects down in array to fill space
for(;index<=File.totalFiles;index++) {
files[index] = files[index + 1]; // This should also set last file to null
}
} else
files[index] = null;
// 3. writeTable() to update table data.
try { // Impossible for IOException when deleting, therefore catch here.
File.writeTable(files);
} catch (IOException e) {}
// 4. Make this file.exists = false and length = 0.
this.exists = false;
this.file_length = 0;
return true;
}
/**
* If the file is a binary executable, begins running it.
*
*/
public void exec() {
Flash.exec(page_location, file_length);
}
/**
* Returns a list of files in the flash file system. Because there are no
* directories, this is a static method in leJOS NXJ. The order of the files
* in the array goes from oldest (0) to newest (highest index array).
*
* @return An array of File objects representing files in the file system.
* The array will be empty if the directory is empty.
*
*/
public static File [] listFiles() {
if(files == null) {
files = new File[MAX_FILES];
File.readTable(files); // Update file data
}
/*File [] retFiles = new File[totalFiles];
for(int i=0;i<retFiles.length;i++) {
retFiles[i] = files[i];
}
return retFiles;*/
return files;
}
/**
* Returns the name of the file.
* @return The name of the file, including the file extension. e.g. "mapdata.txt"
*
*/
public String getName() {
return file_name;
}
/**
* Returns the length of the file denoted by this file name.
* @return The length, in bytes, of the file denoted by this file name, or 0 if the file does not exist.
*/
public long length() {
return (long) file_length;
}
/**
* Indicates if the file exists in the flash memory.
* @return True indicates the file exists, false means it has not been created.
*/
public boolean exists() {
return exists;
}
public boolean canRead() {
return true; // All files can be read in NXJ
}
public boolean canWrite() {
return !((file_attributes & READ_ONLY_ATTR) == READ_ONLY_ATTR);
}
public boolean isHidden() {
return (file_attributes & HIDDEN_ATTR) == HIDDEN_ATTR;
}
public boolean setReadOnly() {
file_attributes = (byte)(file_attributes | READ_ONLY_ATTR);
return true; // Supposed to return false if unsuccessful
}
/**
* Reads the file information in the table from flash memory and
* stores the information in the array supplied.
* @param files An array of File objects. When the method returns the
* array will contain File objects for all the files in flash. If a null
* File array is given, it will create a new File array.
*/
static void readTable(File [] files) {
// Make sure flash has table id:
if(!File.tableExists()) File.format();
File.resetTablePointer();
Flash.readPage(buff, TABLE_START_PAGE); // Kludge to fill data into first page
// Move pointer to file total:
byte_pointer = NUM_FILES_POS; // Kludge
File.totalFiles = readNextByte(); // update total files value
for(int i=0;i<File.totalFiles;i++) {
short pageLocation = (short)((0xFF & readNextByte()) | ((0xFF & readNextByte())<<8));
int fileLength = (0xFF & readNextByte()) | ((0xFF & readNextByte()) <<8) | ((0xFF & readNextByte())<<16) | ((0xFF & readNextByte())<<24);
byte fileAttributes = readNextByte();
// TODO: The following code attempts to reuse String's. If leJOS gets
// a garbage collector we can create new strings and reduce this
// code. It assumes that if files[i] is NOT null then the filename
// is correct. Relies on delete() to adjust file names correctly.
if(files[i] == null) {
byte numChars = readNextByte(); // Size of file name (string length)
for(int j=0;j<numChars;j++) {
charBuff[j] = (char)readNextByte();
}
String name = new String(charBuff, 0, numChars);
files[i] = new File(name, false); // Uses private constructor so it doesn't check through file list if it already exists.
}
files[i].page_location = pageLocation;
files[i].file_length = fileLength;
files[i].file_attributes = fileAttributes;
}
}
/**
* Helper method to read next byte from file table. It
* automatically flips to next page when it gets to end
* of last page.
* @return Next byte of data from table.
*/
private static byte readNextByte() {
if(byte_pointer >= Flash.BYTES_PER_PAGE) {
++page_pointer; // Throw exception here if > FILE_TABLE_PAGES - 1?
byte_pointer = 0;
Flash.readPage(buff, page_pointer);
}
return buff[byte_pointer++];
}
/**
* Writes the file data to the table from the files [] array.
* @param files The array containing a list of Files to write to table.
*/
static void writeTable(File [] files) throws IOException {
/*
* Note: This method doesn't bother assigning empty
* byte positions as 0. Ghost data appears in memory but
* it will be ignored.
*/
// Move pointer to start of file table:
resetTablePointer();
// Write table id (header with version info):
for(int i=0;i<TABLE_ID.length();i++) {
writeNextByte((byte)TABLE_ID.charAt(i));
}
// Write total files (when using GC and arrays, can use array length):
// POSPONED UNTIL LATER (see Kludge below)
writeNextByte((byte)0); // used to increment byte pointer
// Now write all the file info to the table
byte arrayIndex = 0;
if(files != null && files.length != 0) { // Will throw exception for 0 length unless this checks
while(files[arrayIndex] != null) {
if(files[arrayIndex].file_length == -999) break; // !! What is this for? Can't remember why it is here.
try {
// Write page location of file:
writeNextByte((byte)files[arrayIndex].page_location);
writeNextByte((byte)(files[arrayIndex].page_location>>8));
// Write file size:
writeNextByte((byte)files[arrayIndex].file_length);
writeNextByte((byte)(files[arrayIndex].file_length>>8));
writeNextByte((byte)(files[arrayIndex].file_length>>16));
writeNextByte((byte)(files[arrayIndex].file_length>>24));
// Write file attributes:
writeNextByte(files[arrayIndex].file_attributes);
// Write length of name:
writeNextByte((byte)(files[arrayIndex].file_name.length()));
// Write name:
for(int i=0;i<files[arrayIndex].file_name.length();i++) {
writeNextByte((byte)files[arrayIndex].file_name.charAt(i));
}
} catch (IOException e) {
// Write total files (ignoring aborted one) before rethrowing IOException:
// Write the current page to flash:
writeBufftoFlash();
// KLUDGE (should really be done above): Now write total files
Flash.readPage(buff, TABLE_START_PAGE);
buff[NUM_FILES_POS] = arrayIndex; // Update number of files
File.totalFiles = arrayIndex; // Update total files in File class?
Flash.writePage(buff, TABLE_START_PAGE);
throw e;
} finally {
// If catch rethrows IOException wonder if finally called?
}
++arrayIndex;
if(arrayIndex >= files.length) break;
}
}
// Write the current page to flash:
writeBufftoFlash();
// KLUDGE (should really be done above): Now write total files
Flash.readPage(buff, TABLE_START_PAGE);
buff[NUM_FILES_POS] = arrayIndex; // Update number of files
File.totalFiles = arrayIndex; // Update total files in File class?
Flash.writePage(buff, TABLE_START_PAGE);
}
/**
* Couple of global variables used for table pointers.
*/
private static short page_pointer;
private static short byte_pointer;
/**
* Helper method to write the next byte to flash memory
* and automatically switch to next page.
* @param value The value to write.
*/
private static void writeNextByte(byte value) throws IOException {
if(byte_pointer >= Flash.BYTES_PER_PAGE) {
writeBufftoFlash();
++page_pointer;
// Throw exception here if > FILE_TABLE_PAGES - 1:
if(page_pointer >= FILE_TABLE_PAGES){
throw new IOException("File table is full. Try deleting some files.");
}
byte_pointer = 0;
}
buff[byte_pointer] = value;
++byte_pointer;
}
/*
* Old debugger method. Comment out when no longer buggy.
public static void dumpFileTable() {
if(files == null) listFiles(); // Fill list
RConsole.print("byte_pointer = " + byte_pointer + "\n");
RConsole.print("page_pointer = " + page_pointer + "\n");
RConsole.print("FILE_TABLE_PAGES = " + FILE_TABLE_PAGES + "\n");
RConsole.print("files.length = " + files.length + "\n");
RConsole.print("totalFiles = " + totalFiles + "\n");
for(int i=TABLE_START_PAGE;i<FILE_START_PAGE;i++) {
Flash.readPage(buff, i);
for(int j=0;j<Flash.BYTES_PER_PAGE;j++) {
if(j % 8 == 0) RConsole.print("\n");
RConsole.print(buff[j] + ", ");
}
for(int k=0;k<Flash.BYTES_PER_PAGE;k++) {
if(k % 8 == 0) RConsole.print("\n");
RConsole.print((char)buff[k] + " | ");
}
}
RConsole.print("Please copy and paste this into an email to bbagnall@mts.net");
}
*/
/**
* Writes the current page in buff[] to flash
*
*/
private static void writeBufftoFlash() {
Flash.writePage(buff, page_pointer);
}
/**
* Resets the global variables for page and byte pointers to start.
*
*/
private static void resetTablePointer() {
page_pointer = TABLE_START_PAGE;
byte_pointer = 0;
}
/**
* Essentially formats the file system by writing TABLE_ID characters to
* the first page of flash memory. Also writes 0 as the number of files
* in the file system, so it can be used to restart/erase all files.
*/
public static void format() {
// Write TABLE_ID to buff array:
for(int i=0;i<TABLE_ID.length();i++) {
buff[i] = (byte)TABLE_ID.charAt(i);
}
// Write # of files (0) right after TABLE_ID
buff[NUM_FILES_POS] = 0;
Flash.writePage(buff, TABLE_START_PAGE);
// LCD.drawInt(999, 12,5);
files = null;
}
/**
* Creates a new file entry in the flash memory.
* @return True indicates file was created in flash. False means it already existed or the size is 0 or less.
*/
public boolean createNewFile() throws IOException {
/**
* Internally this method updates the page location value and
* adds this file instance to the global array of files.
* It then writes the current files array
* to the file table. It always adds the file to the end
* of the array.
*/
if(exists()) return false; // Exists in file table
if(files == null) {
files = new File[MAX_FILES];
readTable(files); // Update file data
}
// Calculate start page by looking at last File in array
if(File.totalFiles > 0) { // Make sure array not empty
this.page_location = files[File.totalFiles - 1].page_location;
int prevFileSize = files[File.totalFiles - 1].file_length;
if(prevFileSize == 0) prevFileSize = 1; // Kludge to reserve page for empty files.
int pages = prevFileSize / Flash.BYTES_PER_PAGE;
if(prevFileSize % Flash.BYTES_PER_PAGE != 0) pages++;
this.page_location = (short)(page_location + pages);
} else { // If array empty, start writing on first page after table data
this.page_location = File.FILE_START_PAGE;
}
// Add this file to the end of files array.
files[File.totalFiles] = this;
File.writeTable(files); // Now update actual data table
this.exists = true;//file is in the table
return true;
}
/**
* Move the file a page at a time, in order from low to high memory
* assumes that new starting page location is lower in flash memory than the old or else that the new pages
* does not overlap with the old.
* @param page starting page of the new location.
*/
private void moveTo(int page) throws IOException
{
int nrPages = file_length/Flash.BYTES_PER_PAGE;
if(file_length%Flash.BYTES_PER_PAGE>0) nrPages++;
int from = page_location;
int to = page;
page_location =(short) page;
for(int i = 0; i<nrPages;i++)
{
Flash.readPage(buff, from++);
Flash.writePage(buff, to++);
}
writeTable(files);
}
/**
* Move the file to become the last one in flash memory.
*/
public void moveToTop() throws IOException
{
File top = files[totalFiles - 1]; // file at top of flash memory
// !! Is the 1 value below problematic? I want to expand
// past 1 page for table. Actually is looks okay.
int page = 1+ top.getPage()+ (int) top.length()/Flash.BYTES_PER_PAGE;
int length = file_length;
moveTo(page);
delete(); // remove from files[] array
file_length = length;
createNewFile(); // put back into files[]
}
/**
* Returns to total free memory in the flash file system.
*/
public static int freeMemory() {
int last_page;
if(files == null) {
files = new File[MAX_FILES];
File.readTable(files); // Update file data
}
if (totalFiles <= 0) {
last_page = -1;
} else {
File top = files[totalFiles - 1]; // file at top of flash memory
last_page = top.getPage()+((int) top.length()-1)/Flash.BYTES_PER_PAGE;
}
return (Flash.MAX_USER_PAGES - 1 - last_page) * Flash.BYTES_PER_PAGE;
}
/**
* Returns location of file in the files[] array
* @return index of file in files[]
*/
public int getIndex()
{
int i = 0;
while( i<totalFiles && this != files[i]) i++;
return i;
}
/**
* Indicates if the flash memory contains a file table.
* Compares header with expected header (TABLE_HEADER) at the
* start of page 0.
*/
private static boolean tableExists() {
boolean formatted = true;
Flash.readPage(buff, TABLE_START_PAGE);
for(int i=0;i<TABLE_ID.length();i++) {
if(buff[i] != TABLE_ID.charAt(i))
formatted = false;
}
return formatted;
}
/**
* Defrag the file system.
*
* WARNING: should only be called from the startup menu.
* If called from a user program, can cause the current program to
* be moved resulting in a data abort of other firmware crash.
*
* Assumptions: the files[] array has no nulls, and is in increasing order by page_location.
* This scheme moves moves each file down to fill in the empty pages.
*/
// TODO: This isn't a standard Java API method and should not be public. - BB
public static void defrag() throws IOException
{
File file;
int page_pointer = FILE_START_PAGE; // smallest memory location possible for current file
for(byte i = 0; i < totalFiles; i++)
{
file = files[i];
if(file.page_location > page_pointer) file.moveTo(page_pointer);
page_pointer = file.page_location + (int) file.length()/Flash.BYTES_PER_PAGE ;
if (file.length()%Flash.BYTES_PER_PAGE >0 ) page_pointer++;
}
writeTable(files); // update the file data in flash memory
}
/**
* Internal method used to get the page number of the start of the file.
*
* @return page number
*/
// TODO: This isn't a standard Java API method and should not be public. It is used by LCP and Sound. - BB
public int getPage() {
return page_location;
}
/**
* Reset the files array after an error.
* Forces listFiles to read from the file table.
*/
// TODO: This isn't a standard Java API method and should not be public. Used by LCP. - BB
public static void reset() {
files = null;
}
}