Lockers Database - RandomAccessFile Version

We have produced a feasibility prototype for the lockers database, to show how to store the names and homeroom teachers in a 2-D array. We noticed it is pretty annoying to type in test data every time we run the program. Some students found it difficult to do the testing for this reason, and naturally did a poor job of testing as a result.

In a real application, the data needs to be stored permanently. This is called data persistence - the data continues to exist when the program shuts down. The data needs to be saved in a data-file on the hard-disk.

One way to do this is to write the entire array into a sequential text file. This method would be quite acceptable, except for one small danger - if the user forgets to save the data, or if the program crashes before saving, a lot of data could be lost.

A more robust solution is to use a RandomAccessFile. In a RandomAccessFile, each new piece of data can be written directly to the file when it is entered. Then if the program crashes, the data is already saved. This provides high levels of persistence and robustness.

RandomAccessFile Structure and Commands

A RandomAccessFile can be broken up into records corresponding to the rows in an array. Each record is broken up into fields, corresponding to the columns in a 2-D array. Rather than using an index value to find a row in an array, the program must seek to a position in the file in order to read or write a record.

-- Example --

In the file shown below, each record occupies 75 bytes, and contains 3 fields: 32 bytes for the student name, 32 bytes for the homeroom teacher, and 11 bytes for the combination. The apostrophes ` represent blank spaces. The ## signs represent 2 bytes that Java uses to store the length of the following UTF String. This is managed automatically, so the programmer needn't worry about it. Since 32 bytes are available, each String is limited to 30 characters so the ## bytes also fit in the field.

##Chaplin, Charlie``````````````##Mr Smith``````````````````````##11-12-13`
##Schwarzenegger, Governor Arnol##Mr Smith``````````````````````##01-02-04`
```````````````````````````````````````````````````````````````````````````
##John, Olivia``````````````````##Ms Patterson``````````````````##`````````


It appears that the third record is empty, indicating an unassigned locker. The combination for the 4th locker is blank.

The programmer must be careful that strings are small enough to fit in the available space. It appears that Schwarzenegger's name was truncated (the 'd' in "Arnold" is missing) so that it fits in the 30 character limit. If the String is not shortened before writing, it will run off the end of the field and destroy the data in the next field. This is called a buffer over-run. Many Trojan Horse and Virus programs use buffer overruns to crash an operating system so they can take control of the computer. In a data-file, a buffer overrun won't cause a program to crash, but could destroy data.

The following commands assign the 3rd locker to "Mike Earbyter" and fill in the combination field for the fourth locker with "01-01-01". It uses readUTF to check whether the 3rd locker is actually free before writing into that record.

try
{
   RandomAccessFile
data = new RandomAccessFile("lockers.dat");
 
   data.seek(2*75);                    // go to locker #2, name field
   String name = data.readUTF();       // read the name
   if (name.equals(""))                // check that the name is empty
   {
      data.seek(2*75);                 // go to locker #2, name field
      data.writeUTF("Earbyter, Mike"); // write the name
 
      data.seek(2*75 + 32);            // go to locker #2, teacher field
      data.writeUTF("Ms Patterson");   // write the teacher
 
      data.seek(2*75 + 64);            // go to locker #2, combination field
      data.seek("77-77-77");           // write the combination
   }
   data.seek(3*75 + 64);               // go to locker #3, combination field
   data.writeUTF("01-01-01");          // write the combination
   
   data.close();                       // close the file - now the data is "safe"
 
}
catch(IOException e)
{ output(e.toString()); }


Each field is written using a seek command followed by writeUTF. The seek command must tell the exact position in the file by calculating bytes. Notice that the records are numbered starting with 0 (zero), not starting with 1. Also notice that file access can cause an IOException, and this must be handled by the algorithm. One interesting "feature" is that if a record number is larger than the actual file, Java will automatically make the file bigger to accommodate the command. This might sound helpful, preventing indexOutOfBoundsExceptions in a data-file program. But a typing mistake could cause the size of the file to become very large, possibly filling up the entire hard-disk. That could be bad, so the program below checks that the locker number is smaller than 3000 before writing anything.

The program below is a simple prototype for a persistent Lockers Data Base. You should copy the code into a Java file and get it running, and check that it correctly saves the data after the program ends.


import
java.awt.*;     // contains GUI controls
import java.io.*;      // contains data-file commands – for RandomAccessFile

public class LockersDB extends EasyApp
{
   public static void main(String[] args)
   {  new LockersDB();  }
   
   Button bClear = addButton("Clear All",50,50,100,50,this);
   Button bAssign = addButton("Assign",150,50,100,50,this);
   Button bShowNames = addButton("Show Names",250,50,100,50,this);
   
   public LockersDB()
   {
      setSize(500,150);
      setTitle("Lockers Database");
   }      
   
   public void actions(Object source,String command)
   {
      if (source == bClear)
      {  clearList();  }
      else if (source == bAssign)
      {  
         String name = input("Type the name of the student");
         int locker = inputInt("Type the locker number:");
         assignLocker(locker,name);  
      }
      else if (source == bShowNames)
      {  showNames(); }
   }
   
   public void clearList()     // Fills file with blanks
   {
      try
      {  RandomAccessFile data = new RandomAccessFile("lockers.dat","rw");
         for (int rec = 0; rec < 3000; rec = rec+1)
         {
            data.seek(rec*75);      // clear the name field
            data.writeUTF("");

            data.seek(rec*75+32);   // clear the homeroom teacher field
            data.writeUTF("");

            data.seek(rec*75+64);   // clear the combination field
            data.writeUTF("");
         }
         data.close();
      }
      catch (IOException e)
      {  System.out.println("IOException occured in clearList()");  }
   }
   
   public void assignLocker(int locker,String name)   // Put student name into file
   {
      if (
locker >= 3000)
      { return; }

      try
      {  
         RandomAccessFile data = new RandomAccessFile("lockers.dat","rw");

         if (name.length() > 30)
         {  name = name.substring(0,30);  }
         
         data.seek(locker*75);
         data.writeUTF(name);
         
         data.close();
      }
      catch (IOException e)
      {  System.out.println("IOException occured in clearList()");  }  
   }
   
   public void showNames()    // Show non-blank names and lockers
   {     
      try
      {
         RandomAccessFile data = new RandomAccessFile("lockers.dat","rw");
         for (int row = 0; row < 3000; row = row + 1)
         {  
            data.seek(row*75);
            String name = data.readUTF();
            
            if ( !name.equals("") )
            {  
               System.out.println(row + "\t" + name);
            }
         }
         data.close();
      }   
      catch (IOException e)
      {  System.out.println("IOException occured in clearList()");  }  
   }
}




To Do:

  1. Change assignLocker() so it checks that the locker is empty before writing a name into the data file. It needs to use readUTF() to get the name currently in the record, and check that it equals "".

  2. Add parameters "firstLocker" and "lastLocker" to the showNames method, so it can print a smaller list of lockers, like 1001 - 1020. Change the actions method accordingly, so it calls showNames(0 , 2999) .

  3. Add another parameter "teacher" to showNames, and change the method so it only prints lockers that belong to that teacher. It should still handle the "firstLocker" and "lastLocker" parameters correctly.

  4. Change showNames so that whenever the "teacher" parameter equals * , the method prints all the student names regardless of the homeroom teacher in their record.

  5. Add a parameter called "firstLetter" to showNames, so it is possible to print only the students whose name starts with A, or any other letter.

  6. Make the method respond sensibly when firstLetter contains *

  7. Now the users will want to have some nice, simple buttons to click, to get the results they want.
    Make a button for each of the following reports. Be sure to put all input commands in the user interface (actions) -
    don't put input commands into the showNames() method.

    [All names] = prints locker number and names for all occupied lockers - don't print blank names

    [One letter] = input a single letter (e.g. M) and print all the names starting with that letter

    [One homeroom] = input a homeroom teacher's name, and print ALL the lockers (empty or occupied) for that teacher

    [Find empty] = input a firstNumber and lastNumber, and search for and display all the empty lockers between those numbers.

    [Assign to homeroom] = input firstNumber and lastNumber and the name of a homeroom teacher. Assign those lockers to that teacher by writing the teacher's name into each record.

  8. COPY (?!!?) the showNames method to make a printNames() method. This should function EXACTLY the same as showNames(), except that it sends output to a text-file and then to the printer instead of the monitor. Do this as follows:

      PrintWriter file = new PrintWriter(new FileWriter("temp.txt"));

     loop
       print into the file using file.println(...);
     
    end loop
     file.close();

     runProgram("RUNDLL32.EXE MSHTML.DLL,PrintHTML \"temp.txt\"");


    Now whenever the user clicks a report button, they should be asked whether they want to get the results on the screen or on the printer. Then the appropriate method should be executed – showNames() or printNames()


The last command is platform-specific. It only works on a Windows platform. In fact, it might only work under Windows 2000 and no other version of Windows – it has only been tested under Windows 2000. Don't forget to close the text file before printing – otherwise the file won't contain all the data, as the OS usually buffers access to text files and only finishes saving when .close() is executed. If you have trouble with this command, you may need to change some Windows settings in the File Associations, so the OS knows how to print a file automatically. You can get more information about this command at :  http://www.robvanderwoude.com/printfiles.html