A Web application

Bruce Eckel’s Thinking in Java Contents | Prev | Next

Now
let’s consider creating an application to run on the Web, which will show
Java in all its glory. Part of this application will be a Java program running
on the Web server, and the other part will be an
applet
that’s downloaded to the browser. The applet collects information from
the user and sends it back to the application running on the Web server. The
task of the program will be simple: the applet will ask for the email address
of the user, and after verifying that this address is reasonably legitimate (it
doesn’t contain spaces, and it does contain an ‘@’ symbol)
the applet will send the email address to the Web server. The application
running on the server will capture the data and check a data file in which all
of the email addresses are kept. If that address is already in the file, it
will send back a message to that effect, which is displayed by the applet. If
the address isn’t in the file, it is placed in the list and the applet is
informed that the address was added successfully.

Traditionally,
the way to handle such a problem is to create an
HTML
page with a text field and a “submit” button. The user can type
whatever he or she wants into the text field, and it will be submitted to the
server without question. As it submits the data, the Web page also tells the
server what to do with the data by mentioning the
Common
Gateway Interface (CGI) program that the server should run after receiving this
data. This CGI program is typically written in either Perl or C (and sometimes
C++, if the server supports it), and it must handle everything. First it looks
at the data and decides whether it’s in the correct format. If not, the
CGI program must create an HTML page to describe the problem; this page is
handed to the server, which sends it back to the user. The user must then back
up a page and try again. If the data is correct, the CGI program opens the data
file and either adds the email address to the file or discovers that the
address is already in the file. In both cases it must format an appropriate
HTML page for the server to return to the user.

As
Java programmers, this seems like an awkward way for us to solve the problem,
and naturally, we’d like to do the whole thing in Java. First,
we’ll use a Java applet to take care of data validation at the client
site, without all that tedious Web traffic and page formatting. Then
let’s skip the Perl CGI script in favor of a Java application running on
the server. In fact, let’s skip the Web server altogether and simply make
our own network connection from the applet to the Java application on the server!

The
server application

Now
consider the server application, which will be called
NameCollector.
What happens if more than one user at a time tries to submit their email
addresses? If
NameCollector
uses TCP/IP sockets, then it must use the multithreading approach shown earlier
to handle more than one client at a time. But all of these threads will try to
write to a single file where all the email addresses will be kept. This would
require a locking mechanism to make sure that more than one thread
doesn’t access the file at once. A semaphore will do the trick, but
perhaps there’s a simpler way.

When
the server application receives a datagram and unpacks it, it must extract the
email address and check the file to see if that address is there already (and
if it isn’t, add it). And now we run into another problem. It turns out
that Java 1.0 doesn’t quite have the horsepower to easily manipulate the
file containing the email addresses (Java 1.1

does). However, the problem can be solved in C quite readily, and this will
provide an excuse to show you the easiest way to
connect
a non-Java program to a Java program. A
Runtime
object for a program has a method called
exec( )
that will start up a separate program on the machine and return a
Process
object. You can get an
OutputStream
that connects to standard input for this separate program and an
InputStream
that connects to standard output. All you need to do is write a program using
any language that takes its input from standard input and writes the output to
standard output. This is a convenient trick when you run into a problem that
can’t be solved easily or quickly enough in Java (or when you have legacy
code you don’t want to rewrite). You can also use Java’s
native
methods

(see Appendix A) but those are much more involved.


The
C program

The
job of this non-Java application (written in C because Java wasn’t
appropriate for CGI programming; if nothing else, the startup time is
prohibitive) is to manage the list of email addresses. Standard input will
accept an email address and the program will look up the name in the list to
see if it’s already there. If not, it will add it and report success, but
if the name is already there then it will report that. Don’t worry if you
don’t completely understand what the following code means; it’s
just one example of how you can write a program in another language and use it
from Java. The particular programming language doesn’t really matter as
long as it can read from standard input and write to standard output.

//: Listmgr.c
// Used by NameCollector.java to manage 
// the email list file on the server
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BSIZE 250
 
int alreadyInList(FILE* list, char* name) {
  char lbuf[BSIZE];
  // Go to the beginning of the list:
  fseek(list, 0, SEEK_SET);
  // Read each line in the list:
  while(fgets(lbuf, BSIZE, list)) {
    // Strip off the newline:
    char * newline = strchr(lbuf, 'n');
    if(newline != 0)
      *newline = '';
    if(strcmp(lbuf, name) == 0)
      return 1;
  }
  return 0;
}
 
int main() {
  char buf[BSIZE];
  FILE* list = fopen("emlist.txt", "a+t");
  if(list == 0) {
    perror("could not open emlist.txt");
    exit(1);
  }
  while(1) {
    gets(buf); /* From stdin */
    if(alreadyInList(list, buf)) {
      printf("Already in list: %s", buf);
      fflush(stdout);
    }
    else {
      fseek(list, 0, SEEK_END);
      fprintf(list, "%sn", buf);
      fflush(list);
      printf("%s added to list", buf);
      fflush(stdout);
    }
  }
} ///:~ 

This
assumes that the C compiler accepts
‘//’
style comments. (Many do, and you can also compile this program with a C++
compiler.) If yours doesn’t, simply delete those comments.

The
first function in the file checks to see whether the name you hand it as a
second argument (a pointer to a
char)
is
in the file. Here, the file is passed as a
FILE
pointer to an already-opened file (the file is opened inside
main( )).
The function
fseek( )
moves around in the file; here it is used to move to the top of the file.
fgets( )
reads a line from the file
list
into the buffer
lbuf,
not exceeding the buffer size
BSIZE.
This is inside a
while
loop
so that each line in the file is read. Next,
strchr( )
is used to locate the newline character so that it can be stripped off. Finally,
strcmp( )
is
used to compare the name you’ve passed into the function to the current
line int the file.
strcmp( )
returns zero if it finds a match. In this case the function exits and a one is
returned to indicate that yes, the name was already in the list. (Note that the
function returns as soon as it discovers the match, so it doesn’t waste
time looking at the rest of the list.) If you get all the way through the list
without a match, the function returns zero.

In
main( ),
the file is opened using
fopen( ).
The first argument is the file name and the second is the way to open the file;
a+
means “Append, and open (or create if the file does not exist) for update
at the end of the file.” The
fopen( )
function returns a
FILE
pointer which, if it’s zero, means that the open was unsuccessful. This
is dealt with by printing an error message with
perror( )
and terminating the program with
exit( ).

Assuming
that the file was opened successfully, the program enters an infinite loop. The
function call
gets(buf)
gets a line from standard input (which will be connected to the Java program,
remember) and places it in the buffer
buf.
This is simply passed to the
alreadyInList( )
function, and if it’s already in the list,
printf( )
sends that message to standard output (where the Java program is listening).
fflush( )
is a way to flush the output buffer.

If
the name is not already in the list,
fseek( )
is used to move to the end of the list and
fprintf( )
“prints” the name to the end of the list. Then
printf( )
is used to indicate that the name was added to the list (again flushing
standard output) and the infinite loop goes back to waiting for a new name.

Remember
that you usually cannot compile this program on your computer and load it onto
the Web server machine, since that machine might use a different processor and
operating system. For example, my Web server runs on an Intel processor but it
uses Linux, so I must download the source code and compile using remote
commands (via telnet) with the C compiler that comes with the Linux distribution.


The
Java program

This
program will first start the C program above and make the necessary connections
to talk to it. Then it will create a datagram socket that will be used to
listen for datagram packets from the applet.

//: NameCollector.java
// Extracts email names from datagrams and stores
// them inside a file, using Java 1.02.
import java.net.*;
import java.io.*;
import java.util.*;
 
public class NameCollector {
  final static int COLLECTOR_PORT = 8080;
  final static int BUFFER_SIZE = 1000;
  byte[] buf = new byte[BUFFER_SIZE];
  DatagramPacket dp =
    new DatagramPacket(buf, buf.length);
  // Can listen & send on the same socket:
  DatagramSocket socket;
  Process listmgr;
  PrintStream nameList;
  DataInputStream addResult;
  public NameCollector() {
    try {
      listmgr =
        Runtime.getRuntime().exec("listmgr.exe");
      nameList = new PrintStream(
        new BufferedOutputStream(
          listmgr.getOutputStream()));
      addResult = new DataInputStream(
        new BufferedInputStream(
          listmgr.getInputStream()));
 
    } catch(IOException e) {
      System.err.println(
        "Cannot start listmgr.exe");
      System.exit(1);
    }
    try {
      socket =
        new DatagramSocket(COLLECTOR_PORT);
      System.out.println(
        "NameCollector Server started");
      while(true) {
        // Block until a datagram appears:
        socket.receive(dp);
        String rcvd = new String(dp.getData(),
            0, 0, dp.getLength());
        // Send to listmgr.exe standard input:
        nameList.println(rcvd.trim());
        nameList.flush();
        byte[] resultBuf = new byte[BUFFER_SIZE];
        int byteCount =
          addResult.read(resultBuf);
        if(byteCount != -1) {
          String result =
            new String(resultBuf, 0).trim();
          // Extract the address and port from 
          // the received datagram to find out 
          // where to send the reply:
          InetAddress senderAddress =
            dp.getAddress();
          int senderPort = dp.getPort();
          byte[] echoBuf = new byte[BUFFER_SIZE];
          result.getBytes(
            0, byteCount, echoBuf, 0);
          DatagramPacket echo =
            new DatagramPacket(
              echoBuf, echoBuf.length,
              senderAddress, senderPort);
          socket.send(echo);
        }
        else
          System.out.println(
            "Unexpected lack of result from " +
            "listmgr.exe");
      }
    } catch(SocketException e) {
      System.err.println("Can't open socket");
      System.exit(1);
    } catch(IOException e) {
      System.err.println("Communication error");
      e.printStackTrace();
    }
  }
  public static void main(String[] args) {
    new NameCollector();
  }
} ///:~ 

The
first definitions in
NameCollector
should look familiar: the port is chosen, a datagram packet is created, and
there’s a handle to a
DatagramSocket.
The next three definitions concern the connection to the C program: a
Process
object is what comes back when the C program is fired up by the Java program,
and that
Process
object produces the
InputStream
and
OutputStream
objects representing, respectively, the standard output and standard input of
the C program. These must of course be “wrapped” as is usual with
Java IO, so we end up with a
PrintStream
and
DataInputStream.

As
before, a
DatagramSocket
is connected to a port. Inside the infinite
while
loop, the program calls
receive( ),
which blocks until a datagram shows up. When the datagram appears, its contents
are extracted into the
String
rcvd
.
This is trimmed to remove white space at each end and sent to the C program in
the line:

nameList.println(rcvd.trim());

This
is only possible because Java’s
exec( )
provides access to any executable that reads from standard input and writes to
standard output. There are other ways to talk to non-Java code, which are
discussed in Appendix A.

The
NameSender applet

//: NameSender.java
// An applet that sends an email address
// as a datagram, using Java 1.02.
import java.awt.*;
import java.applet.*;
import java.net.*;
import java.io.*;
 
public class NameSender extends Applet
    implements Runnable {
  private Thread pl = null;
  private Button send = new Button(
    "Add email address to mailing list");
  private TextField t = new TextField(
    "type your email address here", 40);
  private String str = new String();
  private Label
    l = new Label(), l2 = new Label();
  private DatagramSocket s;
  private InetAddress hostAddress;
  private byte[] buf =
    new byte[NameCollector.BUFFER_SIZE];
  private DatagramPacket dp =
    new DatagramPacket(buf, buf.length);
  private int vcount = 0;
  public void init() {
    setLayout(new BorderLayout());
    Panel p = new Panel();
    p.setLayout(new GridLayout(2, 1));
    p.add(t);
    p.add(send);
    add("North", p);
    Panel labels = new Panel();
    labels.setLayout(new GridLayout(2, 1));
    labels.add(l);
    labels.add(l2);
    add("Center", labels);
    try {
      // Auto-assign port number:
      s = new DatagramSocket();
      hostAddress = InetAddress.getByName(
        getCodeBase().getHost());
    } catch(UnknownHostException e) {
      l.setText("Cannot find host");
    } catch(SocketException e) {
      l.setText("Can't open socket");
    }
    l.setText("Ready to send your email address");
  }
  public boolean action (Event evt, Object arg) {
    if(evt.target.equals(send)) {
      if(pl != null) {
        // pl.stop(); Deprecated in Java 1.2
        Thread remove = pl;
        pl = null;
        remove.interrupt();
      }
      l2.setText("");
      // Check for errors in email name:
      str = t.getText().toLowerCase().trim();
      if(str.indexOf(' ') != -1) {
        l.setText("Spaces not allowed in name");
        return true;
      }
      if(str.indexOf(',') != -1) {
        l.setText("Commas not allowed in name");
        return true;
      }
      if(str.indexOf('@') == -1) {
        l.setText("Name must include '@'");
        l2.setText("");
        return true;
      }
      if(str.indexOf('@') == 0) {
        l.setText("Name must preceed '@'");
        l2.setText("");
        return true;
      }
      String end =
        str.substring(str.indexOf('@'));
      if(end.indexOf('.') == -1) {
        l.setText("Portion after '@' must " +
          "have an extension, such as '.com'");
        l2.setText("");
        return true;
      }
      // Everything's OK, so send the name. Get a
      // fresh buffer, so it's zeroed. For some 
      // reason you must use a fixed size rather
      // than calculating the size dynamically:
      byte[] sbuf =
        new byte[NameCollector.BUFFER_SIZE];
      str.getBytes(0, str.length(), sbuf, 0);
      DatagramPacket toSend =
        new DatagramPacket(
          sbuf, 100, hostAddress,
          NameCollector.COLLECTOR_PORT);
      try {
        s.send(toSend);
      } catch(Exception e) {
        l.setText("Couldn't send datagram");
        return true;
      }
      l.setText("Sent: " + str);
      send.setLabel("Re-send");
      pl = new Thread(this);
      pl.start();
      l2.setText(
        "Waiting for verification " + ++vcount);
    }
    else return super.action(evt, arg);
    return true;
  }
  // The thread portion of the applet watches for
  // the reply to come back from the server:
  public void run() {
    try {
      s.receive(dp);
    } catch(Exception e) {
      l2.setText("Couldn't receive datagram");
      return;
    }
    l2.setText(new String(dp.getData(),
      0, 0, dp.getLength()));
  }
} ///:~ 

The
UI for the applet is quite simple. There’s a
TextField
in which you type your email address, and a
Button
to send the email address to the server. Two
Labels
are used to report status back to the user.

The
init( )
method sets up the GUI with the familiar layout tools, then creates the
DatagramSocket
that will be used both for sending and receiving datagrams.

Regardless
of whether this is the first time the button was pressed, the text in
l2
is erased.

Once
the name is verified, it is packaged into a datagram and sent to the host
address and port number in the same way that was described in the earlier
datagram example. The first label is changed to show you that the send has
occurred, and the button text is changed so that it reads
“re-send.” At this point, the thread is started up and the second
label informs you that the applet is waiting for a reply from the server.


The
Web page

Of
course, the applet must go inside a Web page. Here is the complete Web page;
you can see that it’s intended to be used to automatically collect names
for my mailing list:

<HTML>
<HEAD>
<META CONTENT="text/html">
<TITLE>
Add Yourself to Bruce Eckel's Java Mailing List
</TITLE>
</HEAD>
<BODY LINK="#0000ff" VLINK="#800080" BGCOLOR="#ffffff">
<FONT SIZE=6><P>
Add Yourself to Bruce Eckel's Java Mailing List
</P></FONT>
The applet on this page will automatically add your email address to the mailing list, so you will receive update information about changes to the online version of "Thinking in Java," notification when the book is in print, information about upcoming Java seminars, and notification about the “Hands-on Java Seminar” Multimedia CD. Type in your email address and press the button to automatically add yourself to this mailing list. <HR>
<applet code=NameSender width=400 height=100>
</applet>
<HR>
If after several tries, you do not get verification it means that the Java application on the server is having problems. In this case, you can add yourself to the list by sending email to
<A HREF="mailto:[email protected]">
[email protected]com</A>
</BODY>
</HTML>

The
applet tag is quite trivial, no different from the first one presented in
Chapter 13.

Problems
with this approach

Firewalls
are conservative little beasts. They demand strict conformance to all the
rules, and if you’re not conforming they assume that you’re doing
something sinful and shut you out (not quite so bad as the Spanish Inquisition,
but close). For example, if you are on a network behind a firewall and you
start connecting to the Internet using a Web browser, the firewall expects that
all your transactions will connect to the server using the accepted http port,
which is 80. Now along comes this Java applet
NameSender,
which is trying to send a datagram to port 8080, which is way outside the range
of the “protected” ports 0-1024. The firewall naturally assumes the
worst – that someone has a virus – and it doesn’t allow the
transaction to happen.

As
long as your customers have raw connections to the Internet (for example, using
a typical Internet service provider) there’s no problem, but you might
have some important customers dwelling behind firewalls, and they won’t
be able to use your program.

This
is rather disheartening after learning so much Java, because it would seem that
you must give up Java on the server and learn how to write CGI scripts in C or
Perl. But as it turns out, despair is not in order.

Since
it’s only for handling requests on the server, the servlet API has no GUI
abilities. This fits quite well with
NameCollector.java,
which doesn’t have a GUI anyway.

More by Author

Must Read