This chapter develops a sample applet that can be used to submit Development Request forms to a database server and to track their progress. It covers many of the aspects that have been covered throughout this book, with an added emphasis on storing the collected information in an Access database.
This chapter features the creation of an applet that replaces a typical office form with a Web site. Any form from any office can be put on a Web site, allowing universal access. This enables a variety of users, from marketing geeks to programmers to managers, to quickly get an idea of the state of certain requests. Figure 24.1 shows an example of a Development Request form.
Figure 24.1 : Example of a request form to be put online.
For the sake of brevity, only a few of the possible fields were added to this example. However, I encourage developers to enhance this applet so that it contains all of the fields required to take that next step toward a paperless office.
The DevRequest applet has three basic screens, as described in
Table 24.1.
| Screen | Purpose |
| Login | Enables the user to log into the system with a username and password. |
| Control | The main screen of the applet, used to enter and review requests in the system. |
| Administration | Enables an administrator of the system to enter and modify both users and departments tracked by the system. |
The login screen simply enables the user to access the system by entering a username and the associated password. It is assumed that the first administrator entry is entered by another means (for example, using Microsoft Access). However, a "back door" could have easily been incorporated into DevRequest. Figure 24.2 shows the login screen.
Figure 24.2 : The DevRequest login screen.
The main screen of the applet is displayed immediately after the user has logged in. Figure 24.3 shows the control screen.
Figure 24.3 : The DevRequest control screen.
Using this screen, the user can see a list of all requests in the system by tracking ID and title in the list at the left of the screen. In addition, if a request is highlighted in the list, the user can select the Details button, displaying all of the information about the request on the right side of the screen. This information includes the title and complete description of the request, when and by whom the request was submitted, the budget in which the resources to complete the request are allocated, and the current status of the request. If the New button is pressed, the currently displayed details will be cleared and the controls will be readied for a new request to be entered. Any modifications that are made to either an existing or new request can be saved to the database by using the Save button located at the bottom of the details form.
When the user is an administrator, the Administration button will bring up the screen in Figure 24.4.
Figure 24.4 : The DevRequest administration screen.
This screen is used to update both the operator and department tables. An operator is simply a user of the system. The department indicates through which budget requests are funded. All of the pertinent information about each of these entities can be updated, or new entities can be added, using this screen.
The classes used in the DevRequest applet are diagrammed in Figure 24.5. Classes whose names are italicized are specific to this applet. Classes without italicized names are standard Java classes used as base classes for DevRequest classes. A short summary for each class can be found in Table 24.2.
Figure 24.5 : DevRequest class overview.
Table 24.2. DevRequest class summary.
| Class | Extends | Description |
| DevRequest | Applet | Main class of the applet that contains the login, control, and administration screens. |
| UserInformation | Utility class that holds information about the current user. Provides a mechanism to control the flow of the screens. | |
| StatusPanel | Panel | Base class for panels in the system providing basic messaging capabilities. |
| LoginPanel | StatusPanel | Contains the controls used to log into the system. |
| ControlPanel | StatusPanel | Contains the controls used to display and modify requests in the system. |
| RequestPanel | StatusPanel | Contains the controls used to display the detail information of a request. |
| Request | Holds information about a single request. Also used to read and write request information from and to the database. | |
| AdministrationPanel | StatusPanel | Contains the controls used to perform administration duties on the database. |
| OperatorPanel | StatusPanel | Contains the controls used to display a list and details of the operators of the system. |
| Operator | Holds information about a single operator. Also used to read and write operator information from and to the database. | |
| DepartmentPanel | StatusPanel | Contains the controls used to display a list and details of the departments of the system. |
| Department | Holds information about a single department. Also used to read and write department information from and to the database. | |
| ListData | List | Associates a data item with each item in the list. |
| ChoiceData | Choice | Associates a data item with each item in the drop-down list. |
The DevRequest applet uses a CardLayout to display each of the three main screens of the system. The information in UserInformation is used to track who is logged onto the system. It controls which of the cards is currently being displayed and what is to be displayed next. Each panel of the CardLayout contains either just controls, or displays its information in additional panels so as to help divide the display functionality into logical groupings. All database access is contained within the low-level entity classes: Request, Operator, and Department. Additional utility classes are provided to aid in displaying messages to the user and associate database information with list items.
The storage of all of the collected information is in a Microsoft
Access database. There are three tables involved, as can been
seen in Table 24.3.
| Table | Description |
| REQUEST | Contains all of the information stored for each request, such as title, submission date, and so on. |
| OPERATOR | Contains all of the information stored for each operator (user) of the system, such as username, password, access level, and so on. |
| DEPARTMENT | Contains all of the information stored for each department, such as department code and name. |
Figure 24.6 contains that physical database model. The arrows on the drawing represent referential integrity constraints. This means that the column from which the arrow originates must exist as a primary key of the table to which it points. For the DevRequest model, the relationship exists that each requester must be an operator in the system. Additionally, the approved budget must come from a department registered in the system.
Figure 24.6 : DevRequest physical database model.
That covers the basics of the applet. The remaining sections of this chapter cover the implementation details.
All of the data for this applet is stored in a Microsoft Access database consisting of three tables: REQUEST, OPERATOR, and DEPARTMENT. The information will be retrieved using the Microsoft Data Access Objects (DAO). For more information on the nuts and bolts of DAO, see Chapter 22, "Using the Data Access Object." In this applet, the information retrieved can be logically grouped based on the tables from which it is being retrieved. Therefore, there are three corresponding Java classes that handle access to these tables: Request, Operators, and Departments.
Request is used to hold and access database information about a single request. This is one of the key classes in the database. There are several members of the class that hold the request information:
public int TrackingId; public Operator Requestor; public String Title; public String Description; public Date SubmissionDate; public Date CompletionDate; public String Status; public Department ApprovedBudget;
Each of the fields corresponds directly to a column in the REQUEST table.
Additionally, two sets of constant values are defined. The first set is for general use and contains values that indicate the state of the object:
public final static Date DATE_NOT_SET = new Date(0); public final static int NEW_REQUEST_ID = -1;
DATE_NOT_SET is used to indicate that a date has not been assigned a value. Because this is a Date instance, it is pretty hard to store a flag in the class to show a special kind of date. However, it is pretty safe to assume that there never will be a submission date or completion date before January 1, 1970, which is the value to which this constant is set. NEW_REQUEST_ID is used to flag that a request is a new request; all existing requests will have an ID greater than 0.
The status of a request can be one of four predefined values. The values that are stored in the database are given by the following constants strings:
public final static String SUBMITTED = "S"; public final static String APPROVED = "A"; public final static String COMPLETED = "C"; public final static String POSTPONED = "P";
There are three key methods in the class: read, write, and readAll. read is used to read the details of a request. It assumes that the tracking ID of the desired request is set when this method is called. Listing 24.1 contains this method.
Listing 24.1. The read method of class Request.
public boolean read(Database db)
{
boolean retval = false; // assume database error
String command = "select * from REQUEST where TRACKING_ID = " +
Integer.toString(TrackingId);
Variant type = new Variant();
Variant options = new Variant();
Variant fieldName = new Variant();
Variant fieldValue = new Variant();
type.putShort(Constants.dbOpenDynaset);
options.putShort(Constants.dbReadOnly);
// create the recordset and get the number of records returned
Recordset recordset = db.OpenRecordset(command, type, options);
int recordCount = recordset.getRecordCount();
// retrieve the first row
if (recordCount > 0) {
recordset.MoveFirst();
// get the fields of the row
Fields fields = recordset.getFields();
_Field field;
// retrieve all of the information and store it
fieldName.putString("TITLE");
field = fields.getItem(fieldName);
fieldValue = field.getValue();
Title = fieldValue.toString();
fieldName.putString("DESCRIPTION");
field = fields.getItem(fieldName);
fieldValue = field.getValue();
Description = fieldValue.toString();
fieldName.putString("REQUESTOR");
field = fields.getItem(fieldName);
fieldValue = field.getValue();
Requestor.setUserName(fieldValue.toString());
fieldName.putString("SUBMISSION_DATE");
field = fields.getItem(fieldName);
fieldValue = field.getValue();
SubmissionDate = new Date(fieldValue.toString());
fieldName.putString("COMPLETION_DATE");
field = fields.getItem(fieldName);
fieldValue = field.getValue();
if (fieldValue.getvt() != Variant.VariantNull)
CompletionDate = new Date(fieldValue.toString());
fieldName.putString("STATUS");
field = fields.getItem(fieldName);
fieldValue = field.getValue();
Status = fieldValue.toString();
fieldName.putString("APPROVED_BUDGET");
field = fields.getItem(fieldName);
fieldValue = field.getValue();
ApprovedBudget.setCode(fieldValue.toString());
retval = true;
}
if (retval)
{
retval = Requestor.read(db);
if (retval)
retval = ApprovedBudget.read(db);
}
return retval;
}
The first task is to determine the defining mechanism for the recordset to be returned. This is accomplished with the following SQL statement, which returns all REQUEST columns for the request with the given ID:
String command = "select * from REQUEST where TRACKING_ID = " +
Integer.toString(TrackingId);
Next, a recordset that contains the information selected by the SQL statement is opened. Since the ID is the primary key of the table, only a single row is returned. Therefore, the record count returned is used to flag the success of the record set retrieval. The next rather large section of code is used to retrieve the individual fields of the row. The process is common throughout the applet and consists of the following steps:
The final task to be completed in the read method is to read in the requester information and the approved budget-department information.
The write method is used to insert a new request into the database or update an existing request. This method assumes that the fields of the current instance of the Request class have been updated with the latest information. Notice that this class has very little, if any, coupling with any interface components being used to gather this information. This allows this class to have the potential of being reused in a number of situations.
Listing 24.2 contains the write method.
Listing 24.2. The write method of class Request.
public void write(Database db)
{
StringBuffer command = new StringBuffer();
// set completion date if needed
if (Status.equals(COMPLETED) && CompletionDate.equals(DATE_NOT_SET))
CompletionDate = new Date();
if (TrackingId == NEW_REQUEST_ID)
{
command.append("insert into REQUEST(TITLE,DESCRIPTION,");
command.append("REQUESTOR,SUBMISSION_DATE,COMPLETION_DATE,");
command.append("STATUS,APPROVED_BUDGET) values(");
command.append("'" + Title + "',");
command.append("'" + Description + "',");
command.append("'" + Requestor.UserName + "',");
command.append("'" + SubmissionDate.toLocaleString() + "',");
command.append((CompletionDate != DATE_NOT_SET) ?
"'" + CompletionDate.toLocaleString() + "'," : "NULL,");
command.append((Status != null) ? "'" + Status + "'," : "NULL,");
command.append("'" + ApprovedBudget.Code + "')");
}
else
{
command.append("update REQUEST set ");
command.append("TITLE = '" + Title + "',");
command.append("DESCRIPTION = '" + Description + "',");
command.append("REQUESTOR = '" + Requestor.UserName + "',");
command.append("SUBMISSION_DATE = '" +
SubmissionDate.toLocaleString() + "',");
command.append("COMPLETION_DATE = ");
command.append((CompletionDate != DATE_NOT_SET) ?
"'" + CompletionDate.toLocaleString() + "'," : "NULL,");
command.append("STATUS = ");
command.append((Status != null) ? "'" + Status + "'," : "NULL,");
command.append("APPROVED_BUDGET = '" + ApprovedBudget.Code + "' ");
command.append("where TRACKING_ID = " +
Integer.toString(TrackingId));
}
Variant options = new Variant();
options.putShort(Constants.dbFailOnError);
db.Execute(command.toString(), options);
}
Because this is the last call before being written out to the database, the completion date of the request is set, if necessary, to the current time. Next, the ID is checked to see if this will be an insert action or an update action. Probably the most straightforward way to perform either of these actions is to write a SQL statement to perform the action, embedding any values necessary directly into the statement. In this way, Variant instances for each field are avoided. The write method creates either an insert statement or update statement based on the value of the ID. Next, the Datebase.Execute method is used to carry out the action.
Finally, the readAll method is used to return a stripped-down version of each request. This is done to minimize the amount of data returned so as not to jeopardize performance. Listing 24.3 contains this method.
Listing 24.3. The readAll method of class Request.
public static boolean readAll(Database db, Vector requestList)
{
boolean retval = false; // assume database error
String command = "select TRACKING_ID, TITLE from REQUEST " + "
order by TRACKING_ID";
Variant type = new Variant();
Variant options = new Variant();
Variant fieldName = new Variant();
Variant fieldValue = new Variant();
type.putShort(Constants.dbOpenDynaset);
options.putShort(Constants.dbReadOnly);
// create the recordset and get the number of records returned
Recordset recordset = db.OpenRecordset(command, type, options);
int retrievedRecordCount = 0;
// retrieve the first row
if (recordset.getRecordCount() > 0)
recordset.MoveFirst();
// read all of the records
while (retrievedRecordCount < recordset.getRecordCount())
{
int trackingId;
String title;
// get the fields of the row
Fields fields = recordset.getFields();
_Field field;
// retrieve all of the information and store it
fieldName.putString("TRACKING_ID");
field = fields.getItem(fieldName);
fieldValue = field.getValue();
trackingId = fieldValue.toInt();
fieldName.putString("TITLE");
field = fields.getItem(fieldName);
fieldValue = field.getValue();
title = fieldValue.toString();
// save the request
requestList.addElement(new Request(trackingId, title));
// move the element indexing
recordset.MoveNext();
retrievedRecordCount++;
}
return retval;
}
Notice that this method is static. This is done so that the method can be called before a Request instance has been allocated. The SQL statement used shows only a limited number of columns being returned:
String command = "select TRACKING_ID, TITLE from REQUEST order by TRACKING_ID";
| TIP |
The order by clause in the Request.readAll method tells Access to return the requests in the order in which the IDs were assigned. This most likely will be the same order in which they were submitted. However, it is much more efficient on larger databases to use the primary key as an ordering column. |
The readAll method is very similar to the read
method with respect to the order in which things are accomplished.
The difference is in the while statement and the fact
that multiple rows are returned, thus cycling through all of the
rows.
| CAUTION |
When retrieving the record count from a recordset, the value returned will always be accurate when dealing with a table-type recordset, and it will be accurate for a dynaset or snapshot only after all records have been returned. Even when moving sequentially through a dynaset, the count returned will not always be a sequentially incrementing count, as would be expected. To handle this, the following code from Request.readAll could be used: Recordset recordset = db.OpenRecordset(command, type, options); If the value returned after opening the recordset is greater than 0, at least one record was returned. Then, as long as the number of records that have been read is less than the record count being returned, continue reading records. |
The public methods of the Request are summarized in Table 24.4.
| Method | Purpose |
| Request() | Constructs a new request. |
| Request(int, String) | Constructs a request representing an existing request with the given ID and title. |
| GetTrackingIdStr() | Returns the ID in a displayable string. |
| GetCompletionDate() | Returns the completion date in a displayable string. |
| ToString() | Returns a string representing this class, containing the ID and title of the request. |
| Clear() | Clears the current request. Upon return, the request looks like a "new" request. |
| set(Request) | Sets the values to the same as the passed in request. |
| Read(Database) | Reads the details of an existing request from the database. |
| Write(Database) | Writes the current request to the database. |
| ReadAll(Database) | Static method used to read the basics of all requests in the system. |
Operator is used to hold information about a single user or operator of the system. The stored information about the operator corresponds to the columns in the OPERATOR table:
public String UserName; public String Password; public String FirstName; public String LastName; public String AccessLevel;
The access level can be used to control the types of services or fields accessible by each operator. The values stored in the database are represented by the following constants in the Operator class:
public final static String ADMINISTRATOR = "A"; public final static String EXECUTIVE = "E"; public final static String MANAGER = "M"; public final static String PROGRAMMER = "P"; public final static String GUEST = "G";
Database access in the Operator class is found through the read, insert, update, and readAll methods.
The read method reads in the operator details for the operator represented by the string found in the UserName member. Listing 24.4 contains the read method.
Listing 24.4. The read method of Operator.
public boolean read(Database db)
{
boolean retval = false; // assume database error
String command = "select * from OPERATOR where USER_NAME = '" +
UserName + "'";
Variant type = new Variant();
Variant options = new Variant();
type.putShort(Constants.dbOpenDynaset);
options.putShort(Constants.dbReadOnly);
// create the recordset and get the number of records returned
Recordset recordset = db.OpenRecordset(command, type, options);
int recordCount = recordset.getRecordCount();
// retrieve the first row
if (recordCount > 0) {
recordset.MoveFirst();
// get the fields of the row
getFields(recordset.getFields());
retval = true;
}
return retval;
}
Because the user name is the primary key of the OPERATOR table, only a single row is returned from the SQL statement:
String command = "select * from OPERATOR where USER_NAME = '" +
UserName + "'";
Once again, even though a dynaset is being returned, because only a single row is expected in the recordset, the record count can be used as a flag to determine if a row was found in the table. To read the values of the row returned, the private method getFields is called. The code for getFields can be found in Listing 24.5.
Listing 24.5. The getFields method of Operator.
private void getFields(Fields fields)
{
Variant fieldName = new Variant();
Variant fieldValue = new Variant();
_Field field;
// retrieve all of the information and store it
fieldName.putString("USER_NAME");
field = fields.getItem(fieldName);
fieldValue = field.getValue();
UserName = fieldValue.toString();
fieldName.putString("PASSWORD");
field = fields.getItem(fieldName);
fieldValue = field.getValue();
Password = fieldValue.toString();
fieldName.putString("FIRST_NAME");
field = fields.getItem(fieldName);
fieldValue = field.getValue();
FirstName = fieldValue.toString();
fieldName.putString("LAST_NAME");
field = fields.getItem(fieldName);
fieldValue = field.getValue();
LastName = fieldValue.toString();
fieldName.putString("ACCESS_LEVEL");
field = fields.getItem(fieldName);
fieldValue = field.getValue();
AccessLevel = fieldValue.toString();
}
This method takes the Fields instance returned for the current record of the recordset. The same field retrieval steps discussed in the previous section are used to retrieve the value for each column and place it in the appropriate class member.
The insert and update methods, shown in Listing 24.6, embed the values being written to the database directly in the SQL statement used to perform the action.
Listing 24.6. The insert and update methods of Operator.
public void insert(Database db)
{
write(db, "insert into OPERATOR values('" + UserName +
"','" + Password + "','" + FirstName + "','" +
LastName + "','" + AccessLevel + "')");
}
public void update(Database db)
{
write(db, "update OPERATOR set PASSWORD = '" + Password +
"', FIRST_NAME = '" + FirstName +
"', LAST_NAME = '" + LastName +
"', ACCESS_LEVEL = '" + AccessLevel +
"' where USER_NAME = '" + UserName + "'");
}
Both of the functions use the private write method to execute the SQL statement. This method simply passes the SQL statement to the appropriate Database method.
private void write(Database db, String command)
{
Variant options = new Variant();
options.putShort(Constants.dbFailOnError);
db.Execute(command, options);
}
Listing 24.7 shows that the readAll method is very similar to Request.readAll. One of the differences is that all of the rows of the table are being returned, so that an open table is used instead of a dynaset. Additionally, getFields is used to retrieve the contents of the fields of each record.
Listing 24.7. The readAll method of Operator.
public static boolean readAll(Database db, Vector operatorList)
{
boolean retval = false; // assume database error
Variant type = new Variant();
Variant options = new Variant();
type.putShort(Constants.dbOpenTable);
options.putShort(Constants.dbReadOnly);
// create the recordset and get the number of records returned
Recordset recordset = db.OpenRecordset("OPERATOR", type, options);
int recordCount = recordset.getRecordCount();
// retrieve the first row
if (recordCount > 0)
recordset.MoveFirst();
// read all of the records
while (recordCount > 0)
{
Operator op = new Operator();
// get the fields of the row
op.getFields(recordset.getFields());
// save the operator
operatorList.addElement(op);
// move the element indexing
recordset.MoveNext();
recordCount-;
}
return retval;
}
The public methods of Operator are summarized in Table
24.5.
| Method | Purpose |
| Operator() | Constructs a new operator. |
| ToString() | Returns a string representing this class, containing the name of the operator. |
| Clear() | Clears the current operator. Upon return, the operator looks like a "new" operator. |
| set(String, String, String, String, String) | Sets the values of the member variables to the parameters passed in. |
| set(Operator) | Sets this operator to an operator with the given operator information. |
| SetUserName(String) | Sets the username of the current operator to the parameter passed in. |
| insert(Database) | Inserts a new row into the database table with the current information. |
| Update(Database) | Updates the row with the operator's information with the given username. |
| Read(Database) | Reads the details of an existing operator from the database. |
| ReadAll(Database) | Static method used to read all operators in the system. |
The final database-access class is used for the DEPARTMENT table. This class is a simpler version of the Operator class. The only information is stored in the two members:
public String Code; public String Name;
The remainder of the class is very similar to the Operator
class, and the public methods are summarized in Table 24.6.
| Method | Purpose |
| Department() | Constructs a new department. |
| ToString() | Returns a string representing this class, containing the name of the department. |
| Clear() | Clears the current department. Upon return, the department looks like a "new" department. |
| set(String, String) | Sets the values of the member variables to the parameters passed in. |
| set(Department) | Sets this department to a department with the given information. |
| SetCode(String) | Sets the code of the current department to the parameter passed in. |
| insert(Database) | Inserts a new row into the database table with the current information. |
| Update(Database) | Updates the row with the department's information using the given department code. |
| Read(Database) | Reads the details of an existing department from the database. |
| ReadAll(Database) | Static method used to read all departments in the system. |
The user interface is created with CardLayout to control the displaying of the three screens in the DevRequest applet. Each contains a single panel, which includes the more sophisticated GribBagLayout to place various controls on the dialog.
During the course of developing the DevRequest applet, it was apparent that there was a need for several common classes. This need included controls to associate a data element with each item in both a Choice and List control, and a common Panel class that provides messaging methods.
To display a list of options on a Panel, a Choice or a List control might be used. When developing more involved applets, there will often be times when the choice made in a Choice or List will imply the use of not only the selected string, but also the entity that that selected string represents. For example, the DevRequest has the potential of displaying a list of all operators in the system in a Choice control for selecting by whom the request was generated. Additional fields could also be added to the DevRequest applet to collect data on who reviewed the request, who approved the request at every management level needed, among other locations throughout the applet. For each of these situations, the logical item to place in the Choice control is the name of the person. However, the name is not what is used to uniquely identify operators. Therefore, you would like to be able to associate the username with each name in the list. Even more appropriate would be to associate an Operator instance with each name in the list.
A simple extension to the Choice class would be to add a Vector that holds an object reference for each item added to the list. Listing 24.8 contains the complete class implementation for the ChoiceData class that associates a data element with each item in the list.
Listing 24.8. The ChoiceData class.
/*
*
* ChoiceData
*
* This class extends the choice control and adds the ability
* to associate a data element with each item in the choice
* list.
*/
public class ChoiceData extends Choice
{
protected Vector Data = new Vector();
public void addItemData(String item, Object obj)
{
addItem(item); // add item to choices
Data.addElement(obj); // add date to the data list
}
public void selectByData(Object obj)
{
// find the data in the data list
int index = Data.indexOf(obj);
// select the corresponding element
select(index);
}
public Object getSelectedData()
{
return getSelectedIndex() >= 0 ?
Data.elementAt(getSelectedIndex()) : null;
}
public void load(Vector data)
{
// add each element to the choice list and the data list
for (Enumeration enum = data.elements();
enum.hasMoreElements();)
{
Object ob = enum.nextElement();
addItemData(ob.toString(), ob);
}
}
}
ChoiceData now allocates a Vector instance, Data, that will be a list of data items associated with each item in the control. Because all of the items are added to the end of the list in the control, the same can be done for the respective data elements. Additionally, the control and the Vector index their respective items with a zero relative offset. Both of these facts enable an easy one-to-one correspondence between the elements in the Choice control and the elements in the Vector.
A new set of data methods are added to the ChoiceData class that operate on both items and data elements. addItemData is used to add a string item to the control and an object to the data list. Both entities are added to the end of their respective lists so that index relationships are maintained. selectByData uses this index relationship to find a data element in the data list and select its corresponding item in the control. Similarly, getSelectedData is used to return the data element associated with the currently selected item in the control.
load is used to fill the control with a list of items in a single method call. This method uses an Enumeration to add each item of the Vector to the control. The toString method of the list elements is used to determine the string displayed in the control.
The corresponding ListData class contains all of the methods described in the ChoiceData class. One additional functionality that the list provides is the ability to clear the list. The ListData equivalent simply has to clear both the control and the data elements:
public void clear()
{
super.clear(); // clear the list
Data.removeAllElements(); // clear the data
}
The DevRequest applet contains a number of Panel-derived classes that are used to logically group controls. There are a couple of concepts used with the DevRequest panels that are not possible with the standard Java classes. These include drawing a border around the perimeter of the panel and displaying a message to the applet status bar. Listing 24.9 contains the listing of the StatusPanel class that provides these capabilities.
Listing 24.9. The StatusPanel class.
/*
*
* StatusPanel
*
* This class provides a simple means to display a status message to
* the applet's status bar.
*/
public class StatusPanel extends Panel
{
protected boolean hasBorder = false;
public StatusPanel()
{
hasBorder = false;
}
public StatusPanel(boolean hasBorder)
{
this.hasBorder = hasBorder;
}
public void paint(Graphics g)
{
if (hasBorder)
{
// draw a rectangle around the panel
Rectangle rect = bounds();
g.drawRect(0, 0, rect.width - 1, rect.height - 1);
}
}
protected void setStatus(String msg)
{
Applet applet = getAppletParent();
applet.showStatus(msg);
}
protected void setStatus(ComException comE)
{
Applet applet = getAppletParent();
if (applet instanceof DevRequest)
{
Variant comError = new Variant();
comError.putShort((short)(comE.getHResult()));
Errors errs = ((DevRequest)applet).dbEngine.getErrors();
dao3032.Error err = errs.getItem(comError);
applet.showStatus("DB Error: " + err.getDescription());
}
else
applet.showStatus(comE.getMessage());
}
private Applet getAppletParent()
{
java.awt.Container parent = getParent();
while (!(parent instanceof Applet))
parent = parent.getParent();
return (Applet)parent;
}
}
The hasBorder member is a flag used in the paint method to draw a simple border around the perimeter of the panel. It is defaulted to not have a border, but it can be modified by passing a parameter to the constructor. The setStatus method is used to place a message on the status bar of the browser the applet is being run from. It uses the private getAppletParent method to retrieve the applet from the parent container of the panel control. getAppletParent assumes that every control eventually has an ancestor that is Applet derived. It uses a while loop to traverse the parent chain and the instanceof operator to find the applet where the panel resides. The second version of the setStatus method takes a COM exception raised by a DAO object and displays the error message associated with the exception.
DevRequest is derived from the Applet class and implements the Observer interface to monitor when the user information has changed so that the appropriate screen can be displayed. The init method first allocates the database connection that will be used throughout the applet. This is done with the following OpenDatabase method:
protected boolean OpenDatabase()
{
URL dbURL;
try {
// otherwise generate it relative to the applet
dbURL = new java.net.URL(getDocumentBase(),
"DevDb.mdb");
}
catch(Exception e) {
showStatus("Error: " + e.getMessage());
return false;
}
// strip "file:/" from dbURL
String filename = dbURL.getFile().substring(1);
// create the database engine
dbEngine = dao_dbengine.create();
// create Variants that will hold parameters that
// will be passed to OpenDatabase
Variant exclusive = new Variant();
Variant readOnly = new Variant();
Variant source = new Variant();
// set parameters for call to OpenDatabase
exclusive.putBoolean(false);
readOnly.putBoolean(false);
source.putString("");
// open the database for non-exclusive access
db = dbEngine.OpenDatabase(filename, exclusive, readOnly, source);
return true;
}
Next, init adds the applet to the observer list of the UserInformation instance. This causes the update method of the applet to be called every time the UserInformation is updated. Finally, the CardLayout is allocated and the panels are added to the applet with the AllocatePanels method:
protected void AllocatePanels()
{
// the main layout manager will be a card layout
layout = new CardLayout();
setLayout(layout);
add(UserInformation.LOGIN, new LoginPanel(db, UserInfo));
add(UserInformation.CONTROL, new ControlPanel(db, UserInfo));
add(UserInformation.ADMINISTRATION, new AdministrationPanel(db, UserInfo));
}
Notice that the name used when adding the panel components is defined as a constant in the UserInformation class. The update method of DevRequest looks at the currently logged-on user information for the next screen to be displayed:
public void update(Observable o, Object arg)
{
// if the user information has changed, assume login
// has been successful and go to control
if (o instanceof UserInformation)
layout.show(this, UserInfo.NextScreen);
}
The UserInformation class is used in conjunction with the DevRequest class to control displaying of the screens at various points in the application. There are two sets of constants defined in the UserInformation class. The first set are the names used for the screens of the dialog:
public final static String LOGIN = "Login"; public final static String CONTROL = "Control"; public final static String ADMINISTRATION = "Administration";
These define the names of the dialogs and are used when signaling the next screen to be displayed. The second set of constants is used when associating the access level of the currently logged-on user with a numerical value. This value can then be queried to determine if certain elements are available to the user. The following is the list of possible values:
public final static int ADMINISTRATOR = 0; // database level public final static int EXECUTIVE = 1; public final static int MANAGER = 2; public final static int PROGRAMMER = 3; public final static int GUEST = 4;
The setAccessLevel method takes an access-level string, presumably read from the database, and associates one of the preceding constants to the current user.
signalNextScreen and done are methods used to notify the observers of this class that the next screen has been changed and needs to be displayed. signalNextScreen is used to control the order in which screens are displayed:
// This method controls the order that the screens are displayed.
public void signalNextScreen()
{
// if at the login or administration screens, then go back
// to the control; otherwise, go to the administration
if ((NextScreen == LOGIN) || (NextScreen == ADMINISTRATION))
NextScreen = CONTROL;
else
NextScreen = ADMINISTRATION;
// notify on lookers for actual changing of the panels
done();
}
In the DevRequest applet, the order of the display of the screens is straightforward and boils down to a simple if statement. If the current screen is not the control screen, then the next screen will be. Otherwise, go to the administration screen. The private done method is used to call the Observable class methods that notify the observers that a change has occurred.
// This method is used to signal all observers that
// this class is done changing.
private void done()
{
setChanged();
notifyObservers();
}
The panels that are added to the applet correspond to the three screens of the applet: Login, Control, and Administration.
LoginPanel is a simple panel that displays text fields to collect the username and password (see Figure 24.2). The layout is performed with a GridBagLayout manager in the constructor:
public LoginPanel(Database db, UserInformation UserInfo)
{
super();
// save the passed in information
this.db = db;
this.UserInfo = UserInfo;
// make a grid bag layout for the panel
GridBagLayout layout = new GridBagLayout();
GridBagConstraints gbc = new GridBagConstraints();
setLayout(layout);
// put a little space above and below the controls
gbc.insets.top = gbc.insets.bottom = 4;
// create the controls for this panel
gbc.weightx = 1.0; // center 2 controls side-by-side
gbc.anchor = GridBagConstraints.EAST;
Label lbl = new Label("User Name:", Label.RIGHT);
layout.setConstraints(lbl, gbc);
add(lbl);
// make the text field the last on the line
gbc.anchor = GridBagConstraints.WEST;
gbc.gridwidth = GridBagConstraints.REMAINDER;
layout.setConstraints(UserNameField, gbc);
add(UserNameField);
gbc.anchor = GridBagConstraints.EAST;
gbc.gridwidth = 1;
lbl = new Label("Password:", Label.RIGHT);
layout.setConstraints(lbl, gbc);
add(lbl);
// make the text field the last on the line
gbc.anchor = GridBagConstraints.WEST;
gbc.gridwidth = GridBagConstraints.REMAINDER;
layout.setConstraints(PasswordField, gbc);
PasswordField.setEchoCharacter('*');
add(PasswordField);
// add the button on its own line
gbc.anchor = GridBagConstraints.CENTER;
Button btn = new Button("Logon");
layout.setConstraints(btn, gbc);
add(btn);
// put all of the space at the bottom
gbc.fill = GridBagConstraints.BOTH;
gbc.weighty = 1.0;
Panel space = new Panel();
layout.setConstraints(space, gbc);
add(space);
}
First, the passed-in information is stored in member variables for use later on in the class. Next, a GridBagLayout is allocated and set as the layout manager for the panel. To add some format to this form, the sets of two controls are added so that the inner sides are centered horizontally on the form. The button is next placed on a line of its own and centered. So that the buttons are not vertically centered, but rather at the top of the form, an empty panel is added on the line following the button. It was given the capability to grow in the vertical direction, yet leave the vertical height of the rest of the controls the same. Therefore, the space panel fills the area below the button and above the bottom of the form.
The action method of the LoginPanel class processes the pushing of the Logon button. It first assumes that both a username and a password are required, so appropriate checks are made. If either field is empty, a message is written to the status bar. Next, the username and password are validated using the following validation routine:
protected boolean ValidateUser()
{
boolean retval = false; // assume no logon
String command = "select * from OPERATOR where USER_NAME = '" +
UserNameField.getText().toLowerCase() +
"' and PASSWORD = '" +
PasswordField.getText().toLowerCase() + "'";
Variant type = new Variant();
Variant options = new Variant();
type.putShort(Constants.dbOpenDynaset);
options.putShort(Constants.dbEditAdd);
// create the recordset
Recordset recordset = db.OpenRecordset(command, type, options);
// if there was a record returned, then get the user's info
if (recordset.getRecordCount() > 0)
{
Variant fieldName = new Variant();
Variant fieldValue = new Variant();
// retrieve the first (and only) row
recordset.MoveFirst();
// get the fields of the row
Fields fields = recordset.getFields();
_Field field;
// retrieve all of the information and store it
fieldName.putString("USER_NAME");
field = fields.getItem(fieldName);
fieldValue = field.getValue();
UserInfo.UserName = fieldValue.toString();
fieldName.putString("FIRST_NAME");
field = fields.getItem(fieldName);
fieldValue = field.getValue();
UserInfo.FirstName = fieldValue.toString();
fieldName.putString("LAST_NAME");
field = fields.getItem(fieldName);
fieldValue = field.getValue();
UserInfo.LastName = fieldValue.toString();
fieldName.putString("ACCESS_LEVEL");
field = fields.getItem(fieldName);
fieldValue = field.getValue();
UserInfo.setAccessLevel(fieldValue.toString());
retval = true; // found an entry
}
return retval;
}
This method is similar to the read method of the Operator class. The distinguishing factor is that the row selected must match both the username and the password, as evident in the SQL statement:
String command = "select * from OPERATOR where USER_NAME = '" +
UserNameField.getText().toLowerCase() +
"' and PASSWORD = '" +
PasswordField.getText().toLowerCase() + "'";
The last task performed by the action processing of the Logon button is to change to the next screen via the UserInformation class.
The ControlPanel is separated into three sections of the screen: the button panel across the top enabling the user to enter a new request, get detailed information about an existing request, or go to the administration screen; a list of all of the currently active requests; and a panel containing the detailed information about the request. Once again, these controls are placed on the dialog using a GridBagLayout manager allocated in the constructor:
public ControlPanel(Database db, UserInformation UserInfo)
{
super();
// save the passed in information
this.db = db;
this.UserInfo = UserInfo;
// make a grid bag layout for the panel
GridBagLayout layout = new GridBagLayout();
GridBagConstraints gbc = new GridBagConstraints();
setLayout(layout);
// put a little space above and below the controls
gbc.insets.top = gbc.insets.bottom = 2;
// create the controls for this panel
gbc.gridwidth = GridBagConstraints.REMAINDER;
AdministrationButton.hide();
Panel p = new Panel();
p.add(new Button("New"));
p.add(new Button("Details"));
p.add(AdministrationButton);
layout.setConstraints(p, gbc);
add(p);
gbc.weightx = 1.0; // equally space remaining
gbc.weighty = 1.0; // let list fill remaining space
gbc.gridwidth = GridBagConstraints.RELATIVE;
gbc.fill = GridBagConstraints.BOTH;
layout.setConstraints(RequestList, gbc);
add(RequestList);
gbc.gridwidth = GridBagConstraints.REMAINDER;
ReqPanel = new RequestPanel(this, db);
layout.setConstraints(ReqPanel, gbc);
add(ReqPanel);
readRequests();
}
The last task performed in the ControlPanel constructor is the reading of the requests from the database using the readRequest method:
public void readRequests()
{
// kill existing list
RequestList.clear();
Vector requests = new Vector();
// query the database for all of the requests
Request.readAll(db, requests);
// load the list
RequestList.load(requests);
// clear the current panel
ReqPanel.setRequest(new Request());
}
Because this method can be called multiple times throughout the life of the panel, the current contents of the list are cleared before adding the new contents. Request.readAll is used to read in all of the requests from the database. The ListData.load method is used to bulk load the list. The last task performed is to clear the contents of the Details panel.
The action method handles the button-press event by sending a newly allocated request to the details panel, sending the data item associated with the selected list item to the details panel, or using the UserInformation class to go to the administration screen.
The show method of the ControlPanel class selectively shows the administration button if the user has the appropriate access level. Notice that this check must be performed in the show method, as opposed to the constructor, because the UserInformation has not been filled in when the constructor gets called.
public void show()
{
if (UserInfo.AccessLevel <= UserInformation.ADMINISTRATOR)
AdministrationButton.show();
super.show();
}
The RequestPanel class represents the detailed information about a request. Figure 24.7 shows the bounds of the RequestPanel class on the control screen.
Figure 24.7 : RequestPanel display on the control screen.
Once again, the placement of the controls on the panel becomes an exercise on the use of a GridBagLayout manager in the constructor of the RequestPanel. Additionally, in the constructor, the following two methods are called to populate the Choice controls with the operators and departments:
private void loadOperators()
{
Vector operators = new Vector();
Operator.readAll(db, operators);
RequestorChoice.load(operators);
}
private void loadDepartments()
{
Vector depts = new Vector();
Department.readAll(db, depts);
ApprovedBudgetChoice.load(depts);
}
Both of these methods use the bulk-loading capabilities of the ChoiceData control.
To display a new request, the setRequest method is called. This method simply saves the request class instance and populates the controls:
public void setRequest(Request req)
{
// save the request as the current request
CurrentRequest = req;
TrackingIdField.setText(CurrentRequest.getTrackingIdStr());
SubmissionDateField.setText(
CurrentRequest.SubmissionDate.toLocaleString());
CompletionDateField.setText(CurrentRequest.getCompletionDate());
TitleField.setText(CurrentRequest.Title);
DescriptionArea.setText(CurrentRequest.Description);
RequestorChoice.select(CurrentRequest.Requestor.toString());
StatusChoice.selectByData(CurrentRequest.Status);
ApprovedBudgetChoice.select(CurrentRequest.ApprovedBudget.toString());
}
To save any details that were modified, the action method responds to the Save button:
public boolean action(Event evt, Object obj)
{
boolean retval = false; // assume no action
if ("Save".equals(obj))
{
if (validateFields())
{
try
{
CurrentRequest.write(db);
conPanel.readRequests();
}
catch(ComException e)
{
setStatus(e);
}
}
retval = true;
}
return retval;
}
Before the details are written to the database, the fields are validated with a call to the validateFields method:
private boolean validateFields()
{
boolean retval = false; // assume not enough info
if (TitleField.getText().length() > 0)
{
CurrentRequest.Title = TitleField.getText();
CurrentRequest.Description = DescriptionArea.getText();
CurrentRequest.Requestor =
(Operator)RequestorChoice.getSelectedData();
CurrentRequest.Status = (String)StatusChoice.getSelectedData();
CurrentRequest.ApprovedBudget =
(Department)ApprovedBudgetChoice.getSelectedData();
retval = true;
}
return retval;
}
DevRequest requires that a title be specified for every request entered in the system. Therefore, the length of the title is checked to make sure one has been entered. If so, then the control information is saved, gathered, and placed in the current request structure before returning to the action method.
After the fields have been validated, the action method writes the information to the database. If no exceptions were raised, the list of the ControlPanel is updated to reflect any changes that may have occurred as a result of saving the request.
The final screen in the system is the Administration screen (see Figure 24.8), which is composed of two panels displaying the operator and department information, and a button used to return to the ControlPanel. Once again, the GridBagLayout manager is used in the constructor:
Figure 24.8 : OperatorPanel display on the Administration screen.
public AdministrationPanel(Database db, UserInformation UserInfo)
{
super();
// save the passed in information
this.db = db;
this.UserInfo = UserInfo;
// make a grid bag layout for the panel
GridBagLayout layout = new GridBagLayout();
GridBagConstraints gbc = new GridBagConstraints();
setLayout(layout);
gbc.weightx = 1.0; // let list fill remaining space
gbc.weighty = 1.0; // let list fill remaining space
gbc.gridwidth = 1;
gbc.gridheight = 2;
gbc.fill = GridBagConstraints.BOTH;
OperatorPanel opPanel = new OperatorPanel(db);
layout.setConstraints(opPanel, gbc);
add(opPanel);
gbc.gridheight = 1;
gbc.gridwidth = GridBagConstraints.REMAINDER;
DepartmentPanel deptPanel = new DepartmentPanel(db);
layout.setConstraints(deptPanel, gbc);
add(deptPanel);
gbc.weightx = 0.0;
gbc.weighty = 0.0;
gbc.anchor = GridBagConstraints.EAST;
gbc.fill = GridBagConstraints.NONE;
Button btn = new Button("Done");
layout.setConstraints(btn, gbc);
add(btn);
}
You might be wondering why the GridBagLayout manager is used so extensively. The reason is that this layout manager is great at laying out controls relative to other controls. Because the majority of controls have some type of relationship to other controls being added to the container, this layout manager is an excellent choice. Also, if additional controls are added later, a lot of the positioning manipulation of existing controls is eliminated-the layout manager takes care of it.
The only other method in AdministrationPanel is the action method that handles the processing of the Done button click and uses the UserInformation class to return to the control screen.
public boolean action(Event evt, Object obj)
{
boolean retval = false; // assume not processed
if ("Done".equals(obj))
{
UserInfo.signalNextScreen();
retval = true;
}
return retval;
}
There are two panels added to the AdministrationPanel: OperatorPanel and DepartmentPanel. As you might have guessed, these panels display operator (Figure 24.8) and department (Figure 24.9) information.
Figure 24.9 : DepartmentPanel display on the Administration screen.
The OperatorPanel uses a, you guessed it, GridBagLayout manager to place an adjustable-height List control above a detail panel and two side-by-side buttons on the panel:
public OperatorPanel(Database db)
{
super(true);
// save the passed in information
this.db = db;
// make a grid bag layout for the panel
GridBagLayout layout = new GridBagLayout();
GridBagConstraints gbc = new GridBagConstraints();
setLayout(layout);
// put a little space above around the controls
gbc.insets.left = gbc.insets.top =
gbc.insets.right = gbc.insets.bottom = 2;
gbc.weighty = 1.0; // let list fill remaining space
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.fill = GridBagConstraints.BOTH;
layout.setConstraints(OperatorList, gbc);
add(OperatorList);
gbc.weighty = 0.0; // don't resize remaining controls
StatusPanel opInfo = getOperatorInfoPanel();
layout.setConstraints(opInfo, gbc);
add(opInfo);
// add two side-by-side buttons
gbc.weightx = 1.0; // equally space horizontally
gbc.gridwidth = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
Button btn = new Button("New");
layout.setConstraints(btn, gbc);
add(btn);
gbc.gridwidth = GridBagConstraints.REMAINDER;
btn = new Button("Update");
layout.setConstraints(btn, gbc);
add(btn);
// read in the operators from the database
readOperators();
}
Additionally, the readOperators method is called to populate the list:
private void readOperators()
{
// kill existing list
OperatorList.clear();
Vector operators = new Vector();
Operator.readAll(db, operators);
OperatorList.load(operators);
}
The action method handles the processing of the buttons:
public boolean action(Event evt, Object obj)
{
boolean retval = false; // assume not processed
if ("New".equals(obj))
{
if (validateFields())
{
try
{
CurrentOperator.insert(db);
readOperators();
}
catch(ComException e)
{
setStatus(e);
}
}
retval = true;
}
else if ("Update".equals(obj))
{
if (validateFields())
{
try
{
CurrentOperator.update(db);
readOperators();
}
catch(Exception e)
{
setStatus("DB Error: " + e.getMessage());
}
}
retval = true;
}
return retval;
}
If the New button is pressed, the fields are validated and moved into the CurrentOperator instance using the validateFields method. The operator is then written to the database as a new row. If an operator is duplicated in the database, then an exception will be raised and the error message will be displayed on the status line of the browser. If the Update button is pressed, the fields are again validated and placed in the CurrentOperator class. The row in the database is then updated with the new information.
The fields are validated by making sure the username has been entered and the two password entries are identical:
private boolean validateFields()
{
boolean retval = false; // assume not enough info
if ((UserNameField.getText().length() > 0) &&
(PasswordField.getText().equals(ConfirmPasswordField.getText())))
{
// move information to the current operator
CurrentOperator.set(UserNameField.getText(),
PasswordField.getText(),
FirstNameField.getText(),
LastNameField.getText(),
(String)AccessLevelChoice.getSelectedData());
retval = true;
}
return retval;
}
The final functionality provided in this class is to automatically populate the details when an operator is selected from the list. This is handled by overriding the handleEvent method.
public boolean handleEvent(Event evt)
{
boolean retval;
switch(evt.id) {
case Event.LIST_SELECT:
case Event.LIST_DESELECT:
fillCurrentOperator();
retval = true;
break;
default:
retval = super.handleEvent(evt);
break;
}
return retval;
}
When an item in a list is selected, an event is generated with an ID of Event.LIST_SELECT. Similarly, when the item is deselected, an Event.LIST_DESELECT event is generated. By trapping both of these events and calling the fillCurrentOperator method every time the selection changes, the details can be modified to show the currently selected operator.
fillCurrentOperator simply queries the list for the selected item and uses the data associated with that item to populate the detail information controls. If there is no item selected, then the List will generate an ArrayIndexOutOfBoundsException exception. This method traps that exception and simply clears the current operator so that the detail fields will be cleared.
private void fillCurrentOperator()
{
try
{
Operator op = (Operator)OperatorList.getSelectedData();
CurrentOperator.set(op);
}
catch (ArrayIndexOutOfBoundsException e)
{
CurrentOperator.clear();
}
UserNameField.setText(CurrentOperator.UserName);
PasswordField.setText(CurrentOperator.Password);
ConfirmPasswordField.setText(CurrentOperator.Password);
FirstNameField.setText(CurrentOperator.FirstName);
LastNameField.setText(CurrentOperator.LastName);
AccessLevelChoice.selectByData(CurrentOperator.AccessLevel);
}
DepartmentPanel is very similar to the OperatorPanel.
This chapter took a first step toward developing an online request system. It involved a multiscreen applet that displayed a number of controls. In addition, the data collected was stored in a database using Microsoft's DAO so that information could be stored for later retrieval. As in any trip, there are many steps to follow, which take you down a number of paths. This applet could be easily modified to include additional controls specific to your needs. Additional types of forms could be included along with the appropriate routing mechanisms, and the database could be expanded or the DAO modified to access existing databases.