next up previous contents
Next: Exercise 2: Guest book Up: Lab 2: Guest book Previous: General information about the

Subsections

Exercise 1: File handling

In this exercise, you will write the file handling capabilities for the guest book.



Part 1: What information do you want?

The first thing you need to do in this exercise is to decide what information you want the user to be able to enter. You must allow the user to enter at least the following information:

However, you are free to add any other information that you may want.

 You must decide on a unique key field for the user information - a field that uniquely identifies the user. A simple solution would be to use the e-mail address, but you are free to choose another field. You could even add two separate account and password fields and check the password whenever the user wants to modify or remove his/her information.

You will need to create a new user information class capable of containing the information you want; when the user submits his/her information, the guest book applet will send a complete user information object over the network. In the lab description, we will call this class UserInfo, but you are of course free to call it whatever you want. The new class should be in the common package, since it needs to be created by the applet (client) and sent to the server.



Part 2: Creating a user database

In the following exercises, the guest book server will need to save user information somewhere. In this exercise, you will write a simple file database that stores a set of UserInfo objects.

One way to do this is simply to store a sequence of UserInfo objects in a file, re-reading the entire file whenever you are searching for a certain user and re-writing the entire file whenever you are adding or modifying a UserInfo object. This is certainly not very efficient. It will be sufficient for an initial implementation, but you would probably like to replace this with a more intelligent implementation some time in the future.

Therefore, it is very important to distinguish between the operations that you need to perform on the database and the implementation of those operations. The operations you will need in this lab are the following:

Exactly how this is done is completely irrelevant to the user of the database, as long as the five operations are available. Therefore, you will separate all database handling code into a class implementing the UserDatabase interface :

package server;

import java.util.Enumeration;

public interface UserDatabase {

    /**
     * Read the entire user database and return an Enumeration containing
     * all UserInfo objects.
     */
    public Enumeration getUsers() throws IOException;

    /**
     * Returns the unique UserInfo object corresponding to the key of
     * the given UserInfo object, or null if no such user exists.
     */
    public common.UserInfo getUser(common.UserInfo info) throws IOException;

    /**
     * If there is no user with the same key as *info*, add *info*
     * to the user database. Otherwise, throw an IOException.
     */
    public void append(common.UserInfo info) throws IOException;
    
    /**
     * If there is a user with the same key as *info*, remove the
     * old user information object and add the new information to
     * the user database. Otherwise, throw an IOException.
     */
    public void update(common.UserInfo info) throws IOException;

    /**
     * If there is a user with the same key as *info*, remove the
     * old user information object.  Otherwise, throw an IOException.
     */
    public void remove(common.UserInfo info) throws IOException;
}

As long as you only use the methods available in UserDatabase, you can begin by writing a very inefficient (but correct!) implementation - called FileDatabase, for example. Later on, you can write a more efficient replacement for the FileDatabase and simply plug it in, since the replacement will also implement the UserDatabase interface.

As you can see, all UserDatabase methods except getUsers() take UserInfo objects as arguments, even though most methods are only interested in the key field. This is because the UserDatabase interface should not impose any restrictions on what key field we use; the methods get the entire UserInfo object and can use any field as a key. Of course, in an implementation of UserDatabase, all methods will use the same key field.

You will need to create a new class implementing the UserDatabase interface; in the lab description, it will be called server.FileDatabase, since it is a user database that uses an ordinary file.

In the UserDatabase interface above, we use a java.util.Enumeration for returning the set of users. An Enumeration has two methods: hasMoreElements() returns true if there are more elements in the sequence, and nextElement() returns the next element in the sequence (see also the TestDatabase class below). This is a nice way to hide the implementation details of your getUsers method: You may read the entire database into memory at once, or you may read each UserInfo object from the file from within the nextElement() method each time it is called.

In this implementation, you can simply create a Vector vec of all users and return vec.elements().

Note that since the server you will write will be threaded, different threads can call methods in the same FileDatabase simultaneously. Therefore, you will need to use synchronization to make sure that the file handler performs atomic operations on the database - that is, the following scenario must not be possible:

1.
Thread A: Calls append; append A reads the entire file into a Vector in memory.
2.
Thread B: Calls append; append B reads the entire file into a Vector in memory.
3.
Thread A: Append A continues, appends user A to its Vector, and writes the Vector to the file.
4.
Thread B: Append B continues, appends user B to its Vector, and writes the Vector to the file.
5.
Information about user A is lost!

How you perform this synchronization is up to you. You can read more about threads and thread synchronization in the Java Tuturial.

(You are allowed to assume that there is only one UserDatabase object accessing the same database file at the same time.)

Storing objects in files

In previous versions of Java, you had to invent your own file formats for writing information to disk. In Java 1.1, however, entire objects (and any ``sub-objects'' they refer to) can be serialized - converted to a stream of bytes - and saved to disk or sent to another program through the network. The serialization protocol is standardized and gives the same results in all Java implementations. You can use serialization in the file database.

Any object that should be serialized must implement the Serializable interface.

To serialize an object (send/write it as a sequence of bytes), you use the writeObject() method in an ObjectOutputStream; this output stream is a filter that is connected to another OutputStream, such as a FileOutputStream or the output stream of a network socket.

You can write any number of objects to an ObjectOutputStream, and you can also write ``composite'' objects. This means that you will need to decide whether you want to write one object at a time, a single array of objects, a Vector of objects, ... You may also want to write some additional information to the stream, such as the number of registered users.

To deserialize the object (receive/read a sequence of bytes and convert it to an object), you use the readObject() method in the corresponding ObjectInputStream.

You can read more about object serialization in the serialization specification in the JDK 1.1 documentation.

As always, you will have to take care of any errors and exceptions that can occur, and you are not allowed to ignore errors silently within the database handler - they should be signaled to the caller as IOExceptions. This means that you may have to catch other exceptions such as ClassNotFoundException in the FileDatabase methods and ``manually'' throw IOExceptions (with readable error message strings) instead.



Part 3: Testing your user database

You can test your file database code with the following code. Note that since your code should detect duplicates, you may need to delete your database file each time you run the test program.

package server;

import common.UserInfo;
import java.util.Vector;
import java.util.Enumeration;

public class TestDatabase {

    public static void main(String[] args) throws IOException {

        /*
         * Declare it only as a UserDatabase -- that way, you are certain
         * that you don't use any methods that are only available in
         * FileDatabase and not in "general" UserDatabases.  One argument
         * to the FileDatabase constructor may be the name of the database
         * file.
         */
        UserDatabase db = new FileDatabase(/* Your code here */)

        UserInfo user1 = new UserInfo(/* Your code here */);
        UserInfo user2 = new UserInfo(/* Your code here */);
        UserInfo user3 = new UserInfo(/* Your code here */);
        UserInfo user4 = new UserInfo(/* Your code here */);

        db.append(user1);
        db.append(user2);
        db.append(user3);
        db.append(user4);

        try {
            db.append(user1);
            // We tried to append the same user again.  If we get this far,
            // we succeeded -- so we have a duplicate.  This is wrong!
            System.out.println("BUG: Failed to detect duplicate user");
            return;
        } catch (IOException e) {
            System.out.println("OK: Detected duplicate user");
        }

        // Here, you should change some non-key fields in user1

        // user1.something = "whatever";

        db.update(user1);

        // And we remove user3...

        db.remove(user3);

        // Then we print the list of users

        Enumeration users = db.getUsers();

        while (users.hasMoreElements() {
            // We only know that we have Objects; they may not be
            // UserInfo objects.  If we tried to cast nextElement()
            // to an UserInfo object here, we could get a
            // ClassCastException.
            Object obj = users.nextElement();

            if (obj instanceof UserInfo) {
                // Yes, it really was a UserInfo object
                printUser((UserInfo)obj);
            } else {
                // For some reason, someone has put something that is not
                // a UserInfo object in our Vector...
                throw new IOException("BUG: Unknown object in user database: " + obj);
            }
        }
    }

    public static void printUser(UserInfo user) {
        System.out.println(/* Your code here */);
    }
}



Comments about the exercise

In this exercise, you have learned how to use files in Java, and specifically how to write objects to and read objects from files using the ObjectOutputStream and ObjectInputStream classes. Java has many other file handling classes. Some of them are the following:

All the stream classes read and write single bytes , even if they return Strings or chars (remember that a Java char is 16 bits). There are also corresponding Reader and Writer classes, introduced in Java 1.1, that are much better at handling true 16-bit characters without loss of information.


next up previous contents
Next: Exercise 2: Guest book Up: Lab 2: Guest book Previous: General information about the
Jonas Kvarnstrom
2/5/1998