On the JabaDot web site (see Section 18.13) there is a
list of 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.
There are several versions of this database, so I have an abstract
class to represent all the user data accessors, called
UserDB
. One of its main functions is to
read the database; this 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. So the design
pattern (see the Introduction to Chapter 8) known
as singleton
(ensure one single
instance exists) 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-1 uses a class called
JDConstants
(JabaDot constants), which is a
wrapper around a Properties
object (see Section 7.4) to get values such as the location of the
database.
Example 20-1. 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 "database" accessors 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. There
are also various “get” methods, such as the one that
finds a user by login name. The basic approach is to open a
BufferedReader
(see Chapter 9), read each line, and (for
non-blank, non-comment lines) construct a
StringTokenizer
(see Section 3.3)
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; one user per line:
#name:passwd:fullname:email:City:Prov:Country:privs admin:secret1:JabaDot Administrator:[email protected]:Toronto:ON:CA:A ian:secret2:Ian Darwin:[email protected]:Toronto:ON:Canada:E
So the UserDBText
class is a
UserDB
implementation that reads this file and
creates a User
object for each non-comment line in
the file. Example 20-2 shows how it works.
Example 20-2. 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.
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 Section 20.4).
18.223.171.51