In this chapter we go through the design of an interactive online telephone book application. First of all, we need to decide which features we want to include in the online telephone book. Of course, we want it to display names and telephone numbers. It would be a nice feature to have a telephone book applet that dials selected numbers. Also, we will want to keep all telephone information on the Internet so that it will be easier to keep this information up-to-date and enable users to access this information from different computer platforms.
We can summarize features we want to be implemented in the telephone book application:
Here are the implementation-specific details:
Strategic decisions we need to make:
Once we selected Java and Web technology as our implementation base, the answer about where to store the data apparently is clear: We will keep it as a document on a Web server. The format does not make a big difference. It could be an HTML document or simply a text file, as long as we can access it over the Net. We will use a very simple format. Name or address information will be delimited from the phone numbers by space or tab symbols. Separate records will be delimited by end-of-line markers. Here is an example of a data file:
Jim (314) 935-81-34 <EOL> Gene 0-117-095-158-5544 <EOL>
How can we dial a phone number? Our first idea would be to simply send the AT command sequence to a modem and have it dial the number over the interface with a telephone line. But what if you do not have a modem attached to your computer or it is busy because you are connected to a BBS or an Internet service provider? A better way would be to dial the number by playing telephone dialing tones on the computer speaker. We always can create or record dialing tones (also called DTMF tones, for Dual Tone Multi-Frequency) and use them when we need to dial a number.
Now lets think about the user interface. Definitely, we will need to display telephone numbers and a searchable list where the user can select a name or address. We will need a dial button. Also, it would be nice to have something like a telephone button pad so that users could enter a phone number that is not found in the telephone book.
We can start designing application components and creating a skeleton for our program. First of all, to have a valid Java application accessible from the Internet browser, we need to include an Applet class. Then, we need a class that incorporates user controls: buttons, edit boxes, and list boxes. Lets call it PhoneControls. To include the functionality of the telephone button pad, we will create a third class called ButtonPad.
The PhoneDial class, derived from the Java Applet class, implements the functionality of Java interactive application capable of communicating with Java-enabled Internet browsers. We will need to customize the default behavior of the Applet class in order to bring telephone book functionality to our users. At this time, the PhoneDial applet does almost nothing. It creates a PhoneControls class that will have telephone book controls, adding this to the center of its own window. Then it passes start and stop notification events to the PhoneControl class in order to enable controls when Applet is started and disable them when it is about to finish.
public class PhoneDial extends Applet { PhoneControls controls; public void init( ) //applet initialization function { String strParam = getParameter(PHONEBOOK); //get argument PHONEBOOK String strPhoneBook = (strParam == null) ? phonebook.html : strParam; //use the default telephone book document name if argument not found controls = new PhoneControls( ); //create controls add(Center, controls); //add controls to the applet } public void start( ) //applet starting { controls.enable( ); //enable controls on applet start } public void stop( ) //applet is about to be closed { controls.disable( ); //disable controls } public static void main(String args[]) { Frame f = new Frame(PhoneDial); //create applet frame PhoneDial phoneDial = new PhoneDial( ); //create a new applet class phoneDial.init( ); //init applet phoneDial.start( ); //start applet f.add(Center, phoneDial); //add applet to the center of allocated window f.resize(150, 200); //resize to preferred dimensions f.show( ); //show applet } }
The function main( ) does a very important job for the applet: It creates a frame for our application and starts a thread where the application actually is running. At first it might seem a little strange. Why should we care about creating a thread for an application if it already is running? The key to understanding this is in the fact that we should return control from the main function, and must do it as fast as possible in order to free up the Internet browser for other useful tasks. At the same time, we will need to keep our application running to process user input, update the screen when necessary, and so on. Luckily, we do not need to write any code to start a thread. This is the default behavior of the Applet class.
We want to add a frame and possibly resize the applet to the dimensions we think would be optimal for our application. We do not expect that we will always get these dimensions. Actually, the browser will negotiate the real size to allocate for the applet during initialization. The application-desired size might be overridden when the actual space available is not large enough or when dimensions are specified in the HTML document that includes calls to the applet.
The last function call we want to add to the main function is show( ). It will send a request to the Internet browser notifying it that our application is ready to be displayed and will cause it to update the part of the screen allocated for our applet.
Two other classes, PhoneControls and ButtonPad, are derived from the Panel class and inherit the functionality of that group of controls. ButtonPad will include telephone buttons and PhoneControls class will include all other elements of user interface: text box for telephone number, list box for selection telephone number from the list of persons and organizations, and the dial button that initiate process of dialing selected number.
class PhoneControls extends Panel //class that will incorporate user controls { ButtonPad controlsButtonPad; //declare class with telephone-style buttons public PhoneControls( ) // PhoneControls class constructor { //TO DO: Add buttons 0 to 9, * and # } } class ButtonPad extends Panel //class with telephone button pad keys { public ButtonPad( ) // ButtonPad class constructor { //TO DO: Add text box for telephone number, list box and Dial button } }
At this time, we included only empty constructors for PhoneControls and ButtonPad classes and placed the ButtonPad variable declaration into the PhoneControls. Note that we have not actually created a new ButtonPad class. We do not need that unless we actually have some buttons in it.
At this time we can compile our project and create an HTML file to test it out. We do not need to have anything in this page but a call to the PhoneDial applet:
<title>PhoneBook</title> <hr> <applet code=PhoneDial.class width=400 height=400> </applet> <hr>
It is no surprise that our application does nothing at this point except display a gray square.
Now we need to forget that we are computer programmers and become an artist for a moment. We need to place our controls so that they will be easy to find. We do not have too many of themjust a button pad, a window where we want to edit the phone number, and a box where we can select a person or organization we want to call. The more people involved in interface design, the more solutions you will have. One of the possible solutions for the user interface is shown on Figure 26.1.
FIGURE 26.1.User interface for the Phone Book and Telephone Dialer application.
Now we can go back to coding and start from the button pad control. First, we need to select the layout. Because all buttons have the same size, GridLayout is the natural choice. We create a GridLayout class and declare the dimensions (four rows and three columns as on your telephone pad) and a gap between buttons.
GridLayout bag = new GridLayout(4, 3, 1, 1);
Now we can assign this layout to our panel as follows:
setLayout(bag);
At this time we can add buttons. To do this, we will create a button, set a label for it, and then add this button into our control panel. We use a small case statement to handle labels for the buttons on the bottom row differentlyinstead of sequential numbers we need to add * , 0, and # to these buttons.
for(int i= 0; i<12; i++) { Button b = new Button( ); if (i<9) setLabel( +String.valueOf(i+1)+ ); else switch(i) { case 9: b.setLabel(*); break; case 10: b.setLabel(0); break; case 11: b.setLabel(#); break; } add(b); }
Now when the button pad is ready, we can place it on the main control panel and start adding other controls. First, we need to declare control elements in the PhoneControl class as follows:
TextField textPhone; Button buttonDial; List listPhone;
We will use the variable textPhone for the text control with the phone number, buttonDial for the large Dial button on the bottom of our form, and listPhone for the list box control where the user can look up the person or organization to call.
For the PhoneControl, we cannot select the same layout used for the button pad because all controls are of different sizes. We have a choice between CardLayout, GridBagLayout, FlowLayout, and BorderLayout. Of course, CardLayout does not fitwe do not want to have the user flipping cards until finding the right control; it is more appropriate for options in setup dialogs. FlowLayout just places controls one after anotherwe do not want that either. With the GridBagLayout, you have the most control over the placement of interface elements, but for this flexibility you will have to pay with extra coding. You will have to set up values that control placement to the GridBagConstraints class. It is not entirely a complex job but just a little boring. We will use BorderLayout because it is simple enough and allows us to control component placement in terms of North, South, East, West, and Center. Now we can set up layout and add our controls to the PhoneControls panel, as follows:
setLayout(new BorderLayout( )); htPhones = new Hashtable( ); listPhone = new List(5, false); add(East, listPhone); add(North, textPhone = new TextField(, 12)); add(West, controls = new ButtonPad( )); add(South, buttonDial = new Button(Dial));
The whole program at this point is shown in Listing 26.1.
/* * Phone Book Application */ import java.awt.*; import java.applet.*; public class PhoneDial extends Applet { PhoneControls controls; public void init( ) { setLayout(new BorderLayout( )); controls = new PhoneControls( ); add(Center, controls); } public void start( ) { controls.enable( ); } public void stop( ) { controls.disable( ); } } public static void main(String args[]) { Frame f = new Frame(PhoneDial); //create an application frame PhoneDial phoneDial = new PhoneDial( ); phoneDial.init( ); //init application phoneDial.start( ); //start application f.add(Center, phoneDial); //add application to the frame f.resize(150, 200); //resize frame f.show( ); //show frame } } class PhoneControls extends Panel { Applet appletParent; ButtonPad controls; TextField textPhone; Button buttonDial; List listPhone; public PhoneControls(Applet appParent, String strPhBook) { appletParent= appParent; setLayout(new BorderLayout( )); htPhones = new Hashtable( ); //create a hash table for phone numbers listPhone = new List(5, false); //create a list to display names add(East, listPhone); //add list box control add(North, textPhone = new TextField(, 12)); //create and add field for //the telephone number add(West, controls = new ButtonPad( ));//add button pad panel add(South, buttonDial = new Button(Dial)); //create Dial button } } class ButtonPad extends Panel { public ButtonPad( ) { //create GridBag layout with 4 rows and 3 columns, 1 pixel gap between //elements GridLayout bag = new GridLayout(4,3, 1, 1); //set layout to ButtonPad panel setLayout(bag); for(int i= 0; i<12; i++) { //create a new button Button b = new Button( ); //set labels to button if (i<9) b.setLabel( +String.valueOf(i+1)+ ); else switch(i) { case 9: b.setLabel(*); break; case 10: b.setLabel(0); break; case 11: b.setLabel(#); break; } //add button to the ButtonPad add(b); } } }
Now, when we have all of our controls in place, we can start adding some real functionality to our applet. First, we need to handle user input. We can do this by handling events that take place when the user pushes buttons or selects an item from the list box.
To do this we will create an Action()method in the PhoneControl applet. It will replace the default Action() method in the Control class that does nothing but pass event notification to the parent class. This function has two arguments. The first argument is the instance of the Event class that includes information about the type of the event, coordinates of the pointer, and event time stamp as well as a bunch of other useful data. The second argument is an event-dependent argument. For the push button events, this argument is a string with a button label that tell us which button causes the event to be fired.
The action method should return a true value when it processes an event, and no other action needs to be done based on this event. It should return a false value when the parent class needs to be notified in order to process the event again. For example, if we wanted to see which button was pressed in the ButtonPad class and at the same time wanted to handle the same push-the-button events in the PhoneControls class, we would implement the action method in the ButtonPad class so that it returned a false value on button notification events.
Here is the code that handles button events:
public boolean action(Event ev, Object arg) { if (ev.target instanceof Button) { String label = (String)arg; if (label.equals(Dial)) DialPhone(textPhone.getText( ).trim( )); else textPhone.setText(textPhone.getText( ).trim( )+label.trim( )); return true; } }
First we determine what kind of event we are working with and whether this event is caused by pushing a dial button. We will call the function DialPhone( ) that will dial a phone number shown by textPhone control. Because the user can enter some spaces around the phone number, we want to trim them out before passing the argument to the DialPhone function. Dont worry about the implementation for the DialPhone function at this timewe will code it later.
If the user selects a button from the button pad, we will append the label from this button to the phone number in textControl. This simply enters the data from the button pad into the phone number field.
Before writing the code that will handle the selection of the list box, we need to think how the phone numbers are stored in our application. Probably, we do not want to show both phone numbers and names at the same time in the listPhone control. It makes more sense to show just the name of the person or organization and leave the phone number information off-screen unless a particular item in the list is selected.
To store the association between names and phone numbers, we will use a hash table. We can populate it from the phone book file and then do a lookup in this table to find the phone number associated with the selected name. To use a hash table we need to import the java.util.Hashtable class into our project and include a variable of Hashtable type called htPhones into the PhoneControl class.
To handle events resulting from the phone number selection on the button pad, we will add the following code to the action method:
if (ev.target instanceof List) { textPhone.setText((String)htPhones.get(listPhone.getSelectedItem())); return true; }
After detecting that the event was created by the list box control we look up the item that is selected on the listPhone. We then use the selected name to get the phone number associated with it from the hash table and pass this phone number to the textPhone control.
This is all we need to do to handle user input.
Our goal is to write a function that will get a document with phone information over the Net.
To create a network connection, we will need to create an instance of the URL class. There are several constructors for the URL class, and we will choose one with two arguments: base network address and relative address. For the base address we will use the address of our applet, which we can obtain through the call to the getDocumentBase() method of applet class. The relative address will be the actual name for the file we want to get and it will be an argument passed to our applet.
After creating a URL connection, we can open a stream to get access to the data.
The following code will open an input stream to the network file strPhoneBook:
//declare an input stream object InputStream is = null; //declare URL URL urlPhBook; try { //open connection to file with name strPhoneBook urlPhBook = new URL(appletParent.getDocumentBase( ), strPhoneBook); //open an input stream is = urlPhBook.openStream( ); /* * Do something useful here */ //close input stream is.close( ); } catch (MalformedURLException e) { //report exception }
Note that we have to catch and handle MalformedURLException in our application because the applet does not have a default handler for most network exceptions. We will not perform any detailed processing of the exception, but will just report it to the user.
The file with names and phone numbers that we keep on the Net will have a very simple format. Each record in this file has two fields: a name or an address, and a phone number. Records will be separated by line separators, either carriage return or a combination of carriage return and line feed so that both UNIX and PC users can use their favorite text editors. Fields in a record will be separated by space or tab symbols. Also, we want to have comments in our file so we will ignore all lines starting with the # symbol and everything after double forward slashes. Here is an example of the phone file acceptable by the applet:
#Our simple phone book work (314) 994 1976 home (314) 863-6688 FAX 314-537-5542 // my work FAX number
To parse the file with phone records, we will use the StreamTokenizer class. StreamTokenizer incorporates a simple parser that will help us to split an input stream into the sequence of tokens. The tokenizer requires a buffered stream on the input so we need to construct a BufferedInputStream based on our InputStream class.
The following code creates a buffered input stream with 4KB of memory allocated for the buffer and an instance of the StreamTokenizer class called st.
StreamTokenizer st = new StreamTokenizer(new BufferedInputStream(is, 4000));
Before the StreamTokenizer can do any useful jobs, we need to define what symbols will be acceptable as regular characters, how we want to designate comments, and whether we want to handle end-of-line symbols differently from others:
st.eolIsSignificant(true); //end of line is a significant symbol st.commentChar(#); //comments will start with # symbol st.slashSlashComments(true); //allow C++ -style comments (//) st.wordChars((, )); // ( and ) are regular characters st.wordChars(-, z); // -, numbers and letters are regular characters
Now we can write an input stream-parsing function that will put name information into the list control and phone numbers into the hash table, as follows:
int nCnt = 0; //record counter htPhones.clear( ); //clear the hash table scan: while (true) //main paring loop switch (st.nextToken( )) //what is our next token? { case StreamTokenizer.TT_EOF: //end of file break scan; //get out of the loop default: //unknown token break; case StreamTokenizer.TT_WORD: //word (any sequence of //characters from the union of //intervals 0..9, A-Z, a-z, or //characters ), ( or -) String strRecord = st.sval; //text: name or address String strValue = ; st.nextToken(); //get next token while (st.ttype != StreamTokenizer.TT_EOL && st.ttype != StreamTokenizer.TT_EOF) //parse to the end of line or to the //end of file - whichever comes first { if (st.ttype == StreamTokenizer.TT_WORD) //text characters from the //phone number: -, ( or ) //symbols { strValue = strValue + st.sval; } else if (st.ttype == StreamTokenizer.TT_NUMBER) //numbers from the //phone number { strValue = strValue + String.valueOf(st.nval); } st.nextToken( ); } nCnt++; //increment phone records counter lb.addItem(strRecord, nCnt); //add name or address to the list box htPhones.put(strRecord, strValue); //add phone number to the hash //table break; //parse next record }//end of parsing loop
The only major piece of code we need to write at this point is the DialPhone() function. This function is called when a user presses the Dial button. The only argument for this function is the phone number the user wants to dial. This function should play the DTMF tones corresponding to this number.
The DTMF signal is a direct algebraic summation of the amplitudes of two waves of different frequencies. For instance, 1 is coded as the sum of the 1209 Hz and 697 Hz signals. A full list of DTMF tones is shown in Table 26.1. We do not want to generate and mix tones on the sound card, so we will use prerecorded audio files with DTMF signals. It is possible to use WaveGen or a similar sound file generating program to generate these files. Assume that you already have audio files and have placed those files in the /audio/ subdirectory.
Table 26.1. DTMF tones.
Telephone key | High tone [Hz] | Low tone [Hz] | Filename |
---|---|---|---|
0 | 1336 | 941 | 0.au |
1 | 1209 | 697 | 1.au |
2 | 1336 | 697 | 2.au |
3 | 1477 | 697 | 3.au |
4 | 1209 | 770 | 4.au |
5 | 1336 | 770 | 5.au |
6 | 1477 | 770 | 6.au |
7 | 1209 | 852 | 7.au |
8 | 1336 | 852 | 8.au |
9 | 1477 | 852 | 9.au |
* | 1209 | 941 | star.au |
# | 1477 | 941 | pound.au |
To play a DTMF sequence corresponding to a telephone number, we want to separate the string with the telephone number into the array of characters and then play the sequence of audio clips with tones based on the appropriate characters.
To separate a string Phone into array sepPhone[], we will use the getChars( ) function from the String class:
char sepPhone[]; sepPhone = new char [phone.length( )]; phone.getChars(0, phone.length( ), sepPhone, 0);
To play an audio clip from the Java applet you need to call the play( ) function, which takes two arguments: the URL that specifies the directory on the Web server with the audio file and the string with the name of the audio file. To get the URL we can call getCodeBase( ), which returns the URL of our applet. The filename we use is based on the telephone key we want to play.
Of course, we do not want to play anything other than the keys from the telephone panel, so we ignore all of the characters from the sepPhone array other than numbers or * and # characters.
for (int i = 0; i < phone.length( ); i++) //loop throughout all keys from sepPhone //array { if ((sepPhone[i] >= 0) && (sepPhone[i] <= 9)) //if we have a valid numeric //key appletParent.play(appletParent.getCodeBase( ), audio/+sepPhone[i]+.au); //play one of files 0.au to 9.au base on sepPhone[i] character else if (sepPhone[i] == *) //star key appletParent.play(appletParent.getCodeBase( ), audio/star.au); //play star.au file else if (sepPhone[i] == #) //pound key appletParent.play(appletParent.getCodeBase( ), audio/pound.au); //play pound.au file }
Like in an auto body shop when the work is almost done, a little more effort needs to be done to make the results look nice. For our application, we will add getAppletInfo( ) and getParameterInfo( ) functions, and add functions to show the current application statusit always pays to be nice to users.
The function getAppletInfo( ) should return a string with copyright, version, and author information. It potentially could be used by Internet browsers to identify applets that could cause problems and reject them. The code is as follows:
public String getAppletInfo( ) { return Phone Book and Dialer. Copyright (C) 1995, 1996 Gene Leybzon; }
The function getParameterInfo( ) returns a two-dimensional string array with a description of applet arguments. Each row of this array has a parameter name, type, and description. Because we have just one parameter for our applet, we will return the 1-by-3 array, as follows:
public String[][] getParameterInfo( ) { String[][] info = { {phonebook, url, phone directory file} }; return info; }
To show applet messages we will use the ShowStatus( ) function that takes a string and puts it into the Applet context. Depending on the Internet browser this information could be shown as a browser information panel or in a separate window with the Java applet log.
Add some more comments and we are done. Listing 26.2 contains the final revision of PhoneDial applet source code.
/* Phone Book Applet */ import java.awt.*; import java.applet.*; import java.io.StreamTokenizer; import java.io.InputStream; import java.io.BufferedInputStream; import java.net.URL; import java.net.MalformedURLException; import java.util.Hashtable; public class PhoneDial extends Applet { /* Phone Book applet */ PhoneControls controls; public void init( ) { String strParam = getParameter(PHONEBOOK); String strPhoneBook = (strParam == null) ? phonebook.html : strParam; setLayout(new BorderLayout( )); controls = new PhoneControls(this, strPhoneBook); add(Center, controls); } public void start( ) { controls.enable( ); } public void stop( ) { controls.disable( ) } public String getAppletInfo( ) { return Phone Book and Dialer. Copyright (C) Gene Leybzon, 1995, 1996; } public String[][] getParameterInfo( ) { String[][] info = { {phonebook, url, phone directory file}, }; return info; } public boolean handleEvent(Event e) { if (e.id == Event.WINDOW_DESTROY) { System.exit(0); } return false; } public static void main(String args[]) { Frame f = new Frame(PhoneDial); PhoneDial phoneDial = new PhoneDial( ); phoneDial.init( ); phoneDial.start( ); f.add(Center, phoneDial); f.resize(150, 200); f.show( ); } } class PhoneControls extends Panel /* Phone Book Controls */ { Applet appletParent; ButtonPad controls; TextField textPhone; Button buttonDial; List listPhone; Hashtable htPhones; public PhoneControls(Applet appParent, String strPhBook) { appletParent= appParent; setLayout(new BorderLayout( )); htPhones = new Hashtable( ); listPhone = new List(5, false); add(East, listPhone); FillListBox(listPhone, strPhBook); listPhone.select(1); add(North, textPhone = new TextField(, 12)); add(West, controls = new ButtonPad()); add(South, buttonDial = new Button(Dial)); } public boolean action(Event ev, Object arg) { if (ev.target instanceof Button) { String label = (String)arg; if (label.equals(Dial)) DialPhone(textPhone.getText( ).trim( )); else textPhone.setText(textPhone.getText( ).trim( )+label.trim( )); return false; } if (ev.target instanceof List) { textPhone.setText((String)htPhones.get(listPhone.getSelectedItem( ))); return false; } return false; } public void DialPhone(String phone) /* Play DTMF tone sequence */ { char sepPhone[]; sepPhone = new char [phone.length( )]; phone.getChars(0, phone.length( ), sepPhone, 0); appletParent.showStatus(Dial in progress ... + phone); for (int i = 0; i < phone.length( ); i++) { if ((sepPhone[i] >= 0) && (sepPhone[i] <= 9)) { appletParent.play(appletParent.getCodeBase( ) ,audio+sepPhone[i]+.au); } else if (sepPhone[i] == *) appletParent.play(appletParent.getCodeBase( ), audio/star.au); else if (sepPhone[i] == #) appletParent.play(appletParent.getCodeBase( ), audio/pound.au); } } public void FillListBox(List lb, String strPhoneBook) /* Get document strPhoneBook from the net and fill list box control */ { InputStream is = null; URL urlPhBook; try { urlPhBook = new URL(appletParent.getDocumentBase( ), strPhoneBook); try { is = urlPhBook.openStream( ); StreamTokenizer st = new StreamTokenizer(new BufferedInputStream(is, 4000)); st.eolIsSignificant(true); st.commentChar(#); st.slashSlashComments(true); st.wordChars((, )); st.wordChars(-, z); int nCnt = 0; htPhones.clear( ); scan: while (true) switch (st.nextToken( )) { case StreamTokenizer.TT_EOF: break scan; default: break; case StreamTokenizer.TT_WORD: String strRecord = st.sval; String strValue = ; st.nextToken(); while (st.ttype != StreamTokenizer.TT_EOL && st.ttype != StreamTokenizer.TT_EOF) { if (st.ttype == StreamTokenizer.TT_WORD) { strValue = strValue + st.sval; } else if (st.ttype == StreamTokenizer.TT_NUMBER) { strValue = strValue + String.valueOf(st.nval); } st.nextToken( ); } nCnt++; lb.addItem(strRecord, nCnt); htPhones.put(strRecord, strValue); break; } is.close( ); } catch(Exception e) { String message = e.toString(); appletParent.showStatus(Exception ... + message); } } catch (MalformedURLException e) { String message = e.toString( ); appletParent.showStatus(Exception ... + message); } } } class ButtonPad extends Panel /* Telephone button pad controls */ { public ButtonPad( ) { GridLayout bag = new GridLayout(4,3, 1, 1); setLayout(bag); for(int i= 0; i<12; i++) { Button b = new Button( ); if (i<9) b.setLabel( +String.valueOf(i+1)+ ); else switch(i) { case 9: b.setLabel(*); break; case 10: b.setLabel(0); break; case 11: b.setLabel(#); break; } add(b); } } }
In this chapter we get through the process of designing and implementing a real-world Java application telephone book and phone number dialer. We have used Java AWT library to create a user interface, and Java applet methods to get information from the Internet and play sound files. Other topics we have discussed include interface layout management, input stream parsing, and event handling.