Recipe 20.2 Text-File Databases


Problem

You wish to treat a text file as a database.

Solution

Write an Accessor class that returns objects of the correct type.

Discussion

The fictional JabaDot web site, like many web sites, has a list of registered users. Each user has a login name, full name, password, email address, privilege level, and so forth and is represented by a User object. These are stored in the User database.

This database has several versions, so I have an abstract class to represent all the user data accessors, called UserDB. UserDB and all its subclasses implement the Data Accessor Object (DAO) design pattern: use a class that encapsulates the complexity of dealing with a particular database. The DAO's main function is to read and write the database; in the text-based version, the reading can be done in the constructor or in the getUsers( ) method.

Of course, for efficiency, we want to do this reading only once, even though we may have many users visiting the site. As a result, the design pattern known as singleton (ensure one single instance exists; see Recipe 9.10) is used. Anybody wanting a UserDB object does not construct one (the constructor is private) but must call getInstance( ). Unsurprisingly, getInstance( ) returns the same value to anyone who calls it. The only implication of this is that some of the methods must be synchronized (see Chapter 24) to prevent complications when more than one user accesses the (single) UserDB object concurrently.

The code in Example 20-2 uses a class called JDConstants (JabaDot constants), which is a wrapper around a Properties object (see Recipe Recipe 7.7) to get values such as the location of the database.

Example 20-2. UserDB.java
package jabadot; import java.io.*; import java.util.*; import java.sql.SQLException;    // Only used by JDBC version import java.lang.reflect.*;        // For loading our subclass class. /** A base for several DAOs for User objects.  * We use a Singleton access method for efficiency and to enforce  * single access on the database, which means we can keep an in-memory  * copy (in an ArrayList) perfectly in synch with the database.  *  * We provide field numbers, which are 1-based (for SQL), not 0 as per Java.  */ public abstract class UserDB {     public static final int NAME        = 1;     public static final int PASSWORD    = 2;     public static final int FULLNAME     = 3;     public static final int EMAIL        = 4;     public static final int CITY        = 5;     public static final int PROVINCE    = 6;     public static final int COUNTRY        = 7;     public static final int PRIVS        = 8;     protected ArrayList users;     protected static UserDB singleton;     /** Static code block to intialize the Singleton. */     static {         String dbClass = null;         try {             dbClass = JDConstants.getProperty("jabadot.userdb.class");             singleton = (UserDB)Class.forName(dbClass).newInstance( );         } catch (ClassNotFoundException ex) {             System.err.println("Unable to instantiate UserDB singleton " +                  dbClass + " (" + ex.toString( ) + ")");             throw new IllegalArgumentException(ex.toString( ));         } catch (Exception ex) {             System.err.println(             "Unexpected exception: Unable to initialize UserDB singleton");             ex.printStackTrace(System.err);             throw new IllegalArgumentException(ex.toString( ));         }     }     /** In some subclasses the constructor will probably load the database,      *  while in others it may defer this until getUserList( ).      */     protected UserDB( ) throws IOException, SQLException {         users = new ArrayList( );     }     /** "factory" method to get an instance, which will always be      * the Singleton.      */     public static UserDB getInstance( ) {         if (singleton == null)             throw new IllegalStateException("UserDB initialization failed");         return singleton;     }     /** Get the list of users. */     public ArrayList getUserList( ) {         return users;     }     /** Get the User object for a given nickname */     public User getUser(String nick) {         Iterator it = users.iterator( );         while (it.hasNext( )) {             User u = (User)it.next( );             if (u.getName( ).equals(nick))                 return u;         }         return null;     }     public synchronized void addUser(User nu) throws IOException, SQLException {         // Add it to the in-memory list         users.add(nu);         // Add it to the on-disk version         // N.B. - must be done in subclass.     }     public abstract void setPassword(String nick, String newPass)      throws SQLException;     public abstract void deleteUser(String nick)     throws SQLException; }

In the initial design, this information was stored in a text file. The UserDB class reads this text file and returns a collection of User objects, one per user. It also has various "get" methods, such as the one that finds a user by login name. The basic approach is to open a BufferedReader (see Chapter 10), read each line, and (for nonblank, noncomment lines) construct a StringTokenizer (see Recipe 3.2) to retrieve all the fields. If the line is well-formed (has all its fields), construct a User object and add it to the collection.

The file format is simple, with one user per line:

#name:passwd:fullname:email:City:Prov:Country:privs admin:secret1:JabaDot Administrator:ian@darwinsys.com:Toronto:ON:CA:A ian:secret2:Ian Darwin:ian@darwinsys.com:Toronto:ON:Canada:E

So the UserDBText class is a UserDB implementation that reads this file and creates a User object for each noncomment line in the file. Example 20-3 shows how it works.

Example 20-3. UserDBText.java
package jabadot; import java.io.*; import java.util.*; import java.sql.SQLException; /** A trivial "database" for User objects, stored in a flat file.  * <P>  * Since this is expected to be used heavily, and to avoid the overhead  * of re-reading the file, the "Singleton" Design Pattern is used  * to ensure that there is only ever one instance of this class.  */ public class UserDBText extends UserDB {     protected final static String DEF_NAME =          "/home/ian/src/jabadot/userdb.txt";     protected String fileName;     protected UserDBText( ) throws IOException,SQLException {         this(DEF_NAME);     }     /** Constructor */     protected UserDBText(String fn) throws IOException,SQLException {         super( );         fileName = fn;         BufferedReader is = new BufferedReader(new FileReader(fn));         String line;         while ((line = is.readLine( )) != null) {             //name:password:fullname:City:Prov:Country:privs             if (line.startsWith("#")) {        // comment                 continue;             }             StringTokenizer st =                 new StringTokenizer(line, ":");             String nick = st.nextToken( );             String pass = st.nextToken( );             String full = st.nextToken( );             String email = st.nextToken( );             String city = st.nextToken( );             String prov = st.nextToken( );             String ctry = st.nextToken( );             User u = new User(nick, pass, full, email,                 city, prov, ctry);             String privs = st.nextToken( );             if (privs.indexOf("A") != -1) {                 u.setAdminPrivileged(true);             }             users.add(u);         }     }     protected PrintWriter pw;     public synchronized void addUser(User nu) throws IOException,SQLException {         // Add it to the in-memory list         super.addUser(nu);         // Add it to the on-disk version         if (pw == null) {             pw = new PrintWriter(new FileWriter(fileName, true));         }         pw.println(toDB(nu));         // toDB returns: name:password:fullname:City:Prov:Country:privs         pw.flush( );     }     protected String toDB(User u) {         // #name:password:fullName:email:City:Prov:Country:privs         char privs = '-';         if (adminPrivs)             privs = 'A';         else if (editPrivs)              privs = 'E';                  return new StringBuffer( )             .append(u.name).append(':')             .append(u.password).append(':')             .append(u.fullName).append(':')             .append(u.email).append(':')             .append(u.city).append(':')             .append(u.prov).append(':')             .append(u.country).append(':')             .append(u.privs)             .toString( );     } }

This version does not have any "set" methods, which would be needed to allow a user to change his/her password, for example. Those will come later.

See Also

If your text-format data file is in a format similar to the one used here, you may be able to massage it into a form where the SimpleText driver (see online source contrib/JDBCDriver-Moss) can be used to access the data using JDBC (see Recipe 20.4).



Java Cookbook
Java Cookbook, Second Edition
ISBN: 0596007019
EAN: 2147483647
Year: 2003
Pages: 409
Authors: Ian F Darwin

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net