Servlets and Custom Clients

 < Free Open Study > 



Servlet applications can be designed to communicate with all types of clients, and HTTP servlet applications with clients over HTTP. To meaningfully communicate, the client and server must agree the protocol and the structure of requests and responses expected.

In this section we are going to see how a Java application can work as the client to a web application. In the application, the client is shown a protected directory by the server, and it can exchange text files with the web application. The client can view the list of files, delete the files, download a copy of the file to their local machine, and upload a file to the server's protected directory.

Servlets and HTTP servlets are often seen as dynamic web page programs. In fact this is only a fraction of what they can do. This application is designed to demonstrate how we can have a servlet communicating with a Java client, sending serialized Java objects and files both ways between the servlet and client. We will also extensively use HTTP status codes in the web application to inform the client of the result of the request.

Using servlets for text-based communications (for example HTML, XML) is relatively straightforward, but sending objects, files, and HTML messages across a network is a little more challenging. We see in this example both sides of the communication, focusing on the servlet side. However, at the end of the day, sending messages between servlets and Java applications is really streaming data, which is very relevant in a distributed computing environment. It may appear to be wrapped in layers of complexity, but the basic principles are the same. This example should unravel many of those layers and expose the mechanisms involved in this type of application.

While this is only a demonstration application, the functionality is taken directly from real world applications. Many trading systems send data (objects, files, and so on) containing the details of completed trades to other systems, which are stored on the server pending processing. If we were anticipating having to communicate with other non-Java systems we could develop XML-based communications, but as this is not the case here we will keep to serializing Java objects and files. By focusing on servlet application development, we should be able to see the skills used for developing in distributed applications.

Designing the Application

The Java Swing-based client application will use the techniques of HTTP tunneling to communicate with the servlet running in Tomcat. The purpose of the application is to provide the client with a view of the files within a protected folder and to allow the client to remotely manage them. There are four key functions that the client will be able to perform in conjunction with the servlet. It must be able to:

  • Get a list of the files in the specified folder

  • Download the files from the server's directory to a local directory

  • Delete files from the server's specified directory

  • Upload files from a local drive to the server's specified directory

The client will only be able to access and perform actions on a specified directory on the server, and on no other. This directory will be %CATALINA_HOME%/webapps/httpServlet/WEB-INF/files/. The WEB-INF directory and all of its contents are not visible to clients. So we have added the files directory, and ten sample text files for use. Our application only permits the transfer of a certain number of text-based files (specified in firewall.common.FileValidator).

You'll learn why the WEB-INF directory is kept private in Chapter 4.

The server will allow the client application access to the contents of a specified directory, and will not provide access to other directories. All communications between the client and server will have to take place over HTTP.

Our application will not have strong security - we will not implement any login or other security. We will learn how to incorporate security into our web applications in Chapter 9.

The Client Graphical User Interface

The client GUI consists of a javax.swing.JList on the top left to hold a list of files in the relevant server directory.

You can learn more about the Swing API in Beginning Java 2 (ISBN: 1-861003-66-8) , as well as from the online documentation at http://java.sun.com/products/jfc/.

The JList is populated when the application loads, and after each action. Pressing the Refresh button will update the contents as well. The top right section is a javax.swing.JTextArea that will display messages to the user. The lower part of the GUI contains buttons to control the application:

click to expand

To upload a file we will present the user with a javax.swing.JFileChooser object to select a file from their system to upload to the server:

click to expand

Once the upload is complete, the user will see a message indicating completion, and the file will appear in the JList:

click to expand

To download a file the user must select a file and hit the Download File button. To keep things simple, we download the file to the user's default directory according to the system. In my case (using Windows 2000) this is C:\Documents and Settings\Andrew.MAIN1\, however in Linux this could be usr/docs/ or similar. File deletion is similar, with the user selecting the file for deletion and then the Delete File button, which gives the user a message to the window once completed.

Planning the Implementation

We will use three packages in our implementation:

  • The firewall.client package contains the classes that are used exclusively by the client. This package does not need to be deployed on the server and can safely be deleted from the server's classes. This can be significant in larger applications where client side only classes and ancillary files such as images can contribute to the server side application bloat.

  • The firewall.server package contains the servlet class and other classes used exclusively by the server. This package need not be deployed as part of the client files, and in a real world scenario should not be included. It would not be included on the client side for two main reasons. Firstly, this package exclusively contains classes that are run on the server and not on the client. Secondly, inclusion of the server-side classes may present a security risk if they were decompiled.

  • The firewall.common package will contain classes that are used on both the server and the client, and this package (and subpackages) must be included in both the server-side deployment and the client-side deployment. There are many types of files that may fall into this category, but two stand out. Firstly all classes that are used in communication between the client and server (the serialized objects) must be common, because both the client and server needs to access them to interpret their data. This also includes data classes (such as list objects) that may be used on both the client and server, and should be included here for consistency. In our case all of these classes are serializable and are used in client-server communication. We have an additional FileValidator helper class as well, which is not serializable. Helper classes, such as debugging classes (logging, assertions), and formatting classes should be included in the common package, if they are used, to ensure uniformity on both the client and server.

Classes Overview

The following tables briefly outline the classes and their role in the application.

The client Package

Class

Role

FileExchangeApplication

Contains the main() method for the application. Acts as 'in loco' controller.

FileExchangeView

Is the GUI of the application.

FileExchangeModel

Contains the business logic for processing user input.

FileExchangeFilter

Minor Filter class for the FileChooser.

The server Package

Class

Role

FileExchangeServlet

The server-side servlet that receives requests for processing.

RequestHandlerFactory

Identifies the incoming request type and allocates and instantiates the appropriate handler object to deal with this request.

RequestHandler

Abstract superclass handler that provides the abstract respond() method, which is implemented in all subclasses to process requests. It also provides other useful methods commonly required by subclasses.

RefreshHandler

Extends RequestHandler to process REFRESH requests.

DownloadHandler

Extends RequestHandler to process DOWNLOAD requests.

UploadHandler

Extends RequestHandler to process UPLOAD requests.

DeleteHandler

Extends RequestHandler to process DELETE requests.

The common Package

Class

Role

BaseRequest

Superclass of all of the serialized objects. Solely stores the request type for the communication.

FileName

Subclass to BaseRequest - includes the filename of the communication.

FileList

Subclass to BaseRequest - includes an array of filenames from the server directory.

FileValidator

Utility class used by both client and server to validate file extensions.

Having defined what we need from our classes, let's now see how we implement them.

Implementing the Client

The FileExchangeView class provides the GUI element, and the FileExchangeModel provides the business logic processing (which is primarily the communication with the server and sending the results to the user).

The FileExchangeApplication Class

This class incorporates the main() method for the application, and is responsible for initializing the FileExchangeView object:

    package firewall.client;    import java.awt.*;    import java.awt.event.*;    import javax.swing.*;    import java.net.*;    public class FileExchangeApplication {      private FileExchangeApplication () {} 

The main() method first checks that the parameter (if any) is a correct server address so that we can access our server. By default the application uses the default server address of http://localhost:8080:

      public static void main(String[] args) {        URL server;        try {          if (args.length > 0) {            server = new URL(args[0] +            "/httpServlet/servlet/firewall.server.FileExchangeServlet");          } else {            server = new URL("http://localhost:8080/httpServlet/servlet/" +                                 "firewall.server.FileExchangeServlet");        } catch (MalformedURLException mue) {          System.out.println("Error in using FileExchangeApp");          System.out.println("Usage:\n " +                             "java firewall.client.FileExchangeApp [ServerURL]"                            );          mue.printStackTrace();          return;        } 

Then we initialize the FileExchangeView object:

        JFrame frame = new FileExchangeView(server,                                           "File Exchange Application");        frame.addWindowListener(new WindowAdapter() {          public void windowClosing(WindowEvent e) {            System.exit(0);          }        });        frame.pack();        frame.setVisible(true);      }    } 

The FileExchangeView Class

The FileExchangeView class sets up the user interface and creates the listeners needed to capture user actions, which in turn pass these on to the FileExchangeModel business logic object:

    package firewall.client;    import java.io.*;    import java.awt.*;    import java.awt.event.*;    import javax.swing.*;    import javax.swing.filechooser.*;    import java.net.*;    public class FileExchangeView extends JFrame { 

First we prepare the URL object that stores a reference to the server's address, and initialize the FileExchangeModel with a reference to this FileExchangeView object:

      URL hostServlet;      FileExchangeModel model = new FileExchangeModel(this); 

Next the GUI elements are initialized:

      final JTextArea messages = new JTextArea(5, 20);      final JList fileList = new JList();      final JFileChooser fc = new JFileChooser();      JButton refreshButton = new JButton("Refresh Filelist");      JButton uploadButton = new JButton("Upload File");      JButton downloadButton = new JButton("Download File");      JButton deleteButton = new JButton("Delete File");      JPanel buttonPanel = new JPanel();      JPanel displayPanel = new JPanel(); 

The constructor initializes and sets up the GUI:

      public FileExchangeView(URL server, String name) {        super(name);        this.hostServlet = server;        //create the message window and the filelist        messages.setMargin(new Insets(5, 5, 5, 5));        messages.setEditable(false);        JScrollPane messageScrollPane = new JScrollPane(messages);        fileList.setSize(1000, 1000);        fileList.setEnabled(true);        fileList          .setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);        JScrollPane fileListScrollPane = new JScrollPane();        fileListScrollPane.getViewport().setView(fileList); 

Then we add the listeners to the each of the four buttons, so that we can process user actions. The listeners pass the request on to the appropriate method from the model object:

        refreshButton.addActionListener(new ActionListener() {          public void actionPerformed(ActionEvent e) {            //call refresh method            model.refreshRequest();            messages.setText("");          }        }); 

Next we add a listener to the upload button:

        uploadButton.addActionListener(new ActionListener() {          public void actionPerformed(ActionEvent e) { 

After setting the file filter to our selected text-based files, we show a dialog which allows the user to select a file to send to the server:

            fc.setDialogTitle("Select file to Upload..........");            FileExchangeFilter filter =              new FileExchangeFilter(FileValidator.VALID_EXTENSIONS,                                                        "Text files");            fc.setFileFilter(filter);            int returnVal = fc.showSaveDialog(FileExchangeView.this); 

Then we perform the user's selected choice:

            if (returnVal == JFileChooser.APPROVE_OPTION) {              File file = fc.getSelectedFile();              messages.append("Uploading: " + file.getName() + "\n");              model.uploadRequest(file);            } else {              messages.append("Upload command cancelled by user.\n");            } 

The next line of code refreshes the list of files after deletion:

            model.refreshRequest();          }        }); 

Next we add listeners to the download and the delete buttons:

        downloadButton.addActionListener(new ActionListener() {          public void actionPerformed(ActionEvent e) {            model.downloadRequest((String) fileList.getSelectedValue());          }        });        deleteButton.addActionListener(new ActionListener() {          public void actionPerformed(ActionEvent e) {            model.deleteRequest((String) fileList.getSelectedValue());          }        }); 

The individual objects of the GUI are then added to their respective panels, which are in turn added to this JFrame:

        buttonPanel.add(refreshButton);        buttonPanel.add(uploadButton);        buttonPanel.add(downloadButton);        buttonPanel.add(deleteButton);        refreshButton.setNextFocusableComponent(uploadButton);        uploadButton.setNextFocusableComponent(downloadButton);        downloadButton.setNextFocusableComponent(deleteButton);        deleteButton.setNextFocusableComponent(refreshButton);        displayPanel.setLayout(new GridLayout(1, 2));        displayPanel.add(fileListScrollPane);        displayPanel.add(messageScrollPane);        Container contentPane = getContentPane();        contentPane.setLayout(new BorderLayout());        contentPane.add(buttonPanel, BorderLayout.CENTER);        contentPane.add(displayPanel, BorderLayout.NORTH); 

Then we setup the filelist:

        model.refreshRequest();      } 

We need an accessor method for the host servlet's address.

      public URL getHostServlet() {        return hostServlet;      } 

Finally, we need an appendMessage() method that allows the FileExchangeModel to update the messages for the user. We also need refreshFileList() method, that takes a String array as input parameter, to update the list of files in the FileExchangeView:

      public void appendMessage(String message) {        messages.append(message + "\n");      }      public void refreshFileList(String[] files) {        fileList.setListData(files);      }    } 

The FileExchangeModel Class

This class is designed to perform the application's business logic. The FileExchangeView essentially focuses on the GUI aspect, while the object of this class is called to process user actions according to the application's business rules:

    package firewall.client;    import firewall.common.*;    import java.io.*;    import java.net.*;    public class FileExchangeModel { 

First the model needs a reference to the FileExchangeView, so that once it has completed processing the user requests it can send the results to this object for display to the user:

      FileExchangeView view;      public FileExchangeModel(FileExchangeView view) {        this.view = view;      } 

There are four major methods in this class, corresponding with the four buttons and actions a user can request from the server. Each of these methods is responsible for preparing its request to send to the server, retrieving the response, and displaying an appropriate message indicating the outcome of the request.

The uploadRequest() method is a request to upload a file from the client application to the server. Once the filename is validated, the process that takes place is that a FileName object indicating the filename and the request type is sent to the server, followed by the streamed file. Then the server returns an HTTP status code, indicating whether the upload was successful. Finally the method requests a refresh of the file list in the FileExchangeView and the user should see their file, assuming the request was processed successfully:

      public void uploadRequest(File uploadFile) {        if (!FileValidator.isFileNameValid(uploadFile)) {            view.appendMessage("Upload:Error - File selected is invalid");            return;        }        URL url = view.getHostServlet();        HttpURLConnection httpURLConnection = null;        int status = 0;        String message = null;        try { 

Here is where the application starts the process of sending a HTTP POST request to our servlet. Using the URL object we open a connection to the servlet with the getHttpURLConnection() method. Calling the sendRequest() method, we send our request object to the server:

          httpURLConnection = getHttpURLConnection(url);          sendRequest(httpURLConnection,                      new FileName(BaseRequest.UPLOAD, uploadFile.getName()),                      false); 

This application is designed to exchange text files between client and server. We could of course modify this to work with any type of file, by using different I/O classes to read and send the data.

Next the file that we have chosen must be read in and sent out to the server. We create a FileReader object to read from the selected file, and a PrintWriter object (wrapping the connection's OutputStream). Then the FileReader reads the file to the PrintWriter object, which sends it out to the server. We then close the FileReader and output objects:

          //now send the file to the client          FileReader fileReader = new FileReader(uploadFile);          OutputStream os = httpURLConnection.getOutputStream();          PrintWriter out = new PrintWriter(os);          char c[] = new char[4096];          int read = 0;          // Read until end of file and send to client          while ((read = fileReader.read(c)) != -1) {            out.write(c, 0, read);          }          // Close          fileReader.close();          out.close();          os.close(); 

We must read back the server's response, so that we get the HTTP response code and message:

          status = httpURLConnection.getResponseCode();          message = httpURLConnection.getResponseMessage();        } catch (Exception e) {          e.printStackTrace();        } 

Finally we inform the FileExchangeView of the result of the request, and refresh the file list to reflect the new file. HTTP status codes from 200-299 indicates a successful response from the server,so if we use the constants HttpURLConnection.HTTP_OK (200 "OK") and HttpURLConnection.HTTP_MULT_CHOICE (300) as boundaries, we can check if the response is successful:

        //inform the client        if (status >= HttpURLConnection.HTTP_OK ||            status < HttpURLConnection.HTTP_MULT_CHOICE) {          view.appendMessage("Upload:Success (" + status + ") " + message);        } else {          view.appendMessage("Upload:Error (" + status + ") " + message);        }        //refresh the file list        refreshRequest();      } 

The downloadRequest() method is a request from the client to download a file from the server. The process that takes place is that a FileName object, indicating the filename wanted, and the request type is sent to the server. Then the server returns the file in its response. The location that the file is saved to is determined by the location of the user's default directory. In any case the full location of the file is displayed to the user:

      public void downloadRequest(String fileName) {        //ensure valid file is selected        if (!FileValidator.isFileNameValid(fileName)) {          view.appendMessage("Download:File must be selected");          return;        } 

After ensuring that a file was actually selected, the location where the file should be saved the file is selected via the users default directory ("user.home"). This simply maps to the users home directory, irrespective of the operating system being used, which is where we save files:

        URL url = view.getHostServlet();        HttpURLConnection httpURLConnection = null;        //default location to save file to        File saveFile = new File(System.getProperty("user.home"), fileName);        try { 

As we did previously, we call the getHttpURLConnection() method to get a connection to the server, and then call our sendRequest() method sendRequest() method to make the request to download the requested file:

          httpURLConnection = getHttpURLConnection(url);          sendRequest(httpURLConnection,                      new FileName(BaseRequest.DOWNLOAD, fileName)); 

Next we check the status code for success, and update the user:

          int status = httpURLConnection.getResponseCode();          String message = httpURLConnection.getResponseMessage();          //inform the client          if (status >= HttpURLConnection.HTTP_OK ||              status < HttpURLConnection.HTTP_MULT_CHOICE) {            view.appendMessage("Download:Success (" + status + ") " + message);          } else {            view.appendMessage("Download:Error (" + status + ") " + message);            return;          } 

We then get an InputStreamReader, wrapped in a BufferedReader (for efficiency), to read the streamed file, while the FileWriter object writes the received file to our user directory. Note that, as we mentioned before, this is suitable for receiving text files only, but could be adapted for all files if required:

          InputStreamReader isr =            new InputStreamReader(httpURLConnection.getInputStream());          BufferedReader bfr = new BufferedReader(isr);          FileWriter fileWriter = new FileWriter(saveFile);          char c[] = new char[4096];          int read = 0;          while ((read = bfr.read(c)) != -1) {            fileWriter.write(c, 0, read);          } 

Finally the FileWriter and InputStream objects are closed, and a message is returned to the FileExchangeView to let the user know where the file was saved:

          fileWriter.close();          isr.close();          view.appendMessage("Download:File saved to:\n" +                              saveFile.getCanonicalPath());        } catch (Exception e) {          view.appendMessage("Download:Error downloading the file");          e.printStackTrace();        }      } 

The refreshRequest() method (that we've already used) is a request from the client to refresh the list of files from the server. The process that takes place is that a BaseRequest object, indicating that the request type, is sent to the server. Then the server returns the array of String filenames in the response, for display to the user:

      public void refreshRequest() {        URL url = view.getHostServlet();        HttpURLConnection httpURLConnection = null;        FileList files = null;        try { 

The first part of the request is as in the previous methods. A BaseRequest object indicating a REFRESH request is then sent to the server:

          httpURLConnection = getHttpURLConnection(url);          sendRequest(httpURLConnection,                      new BaseRequest(BaseRequest.REFRESH)); 

The server's response status code is checked:

          if (status >= HttpURLConnection.HTTP_OK ||              status < HttpURLConnection.HTTP_MULT_CHOICE) {            view.appendMessage("Refresh:Success (" + status + ") " + message);          } else {            view.appendMessage("Refresh:Error (" + status + ") " + message);            return;          } 

The response from the server this time is a FileList object, which contains the array of String filenames, and the JList object on screen is refreshed with these:

          files = (FileList) readResponse(httpURLConnection);          if (files == null) {            view.appendMessage("Refresh:Error - directory is null");          } else {            //refresh the filelist on the screen            FileValidator.cleanFileList(files.getFileList());            view.refreshFileList(files.getFileList());          }        } catch (Exception e) {          e.printStackTrace();          view.appendMessage("Refresh:Error refreshing the filelist");          view.appendMessage("Using URL:" + url);          view.appendMessage("Check the URL to the servlet including port");        }      } 

The fourth and final important method is deleteRequest(). This is a request from the client to delete a file on the server. The process this time is that a FileName object, indicating the file to be deleted, and the request type, is sent to the server. Then the server returns a status code indicating if the file was successfully deleted. Finally, the refreshRequest() method is called to update the listing of files in the FileExchangeView:

      public void deleteRequest(String fileName) {        //check the file is correct and selected        if (!FileValidator.isFileNameValid(fileName)) {          view.appendMessage("Delete:File not selected. Please select file");        } else {          URL url = view.getHostServlet();          HttpURLConnection httpURLConnection = null;          String message = null;          int status = 0;          try { 

The FileName object is sent to the server indicating the file to delete:

            httpURLConnection = getHttpURLConnection(url);            sendRequest(httpURLConnection,                        new FileName(FileName.DELETE, fileName)); 

The HTTP status code is read back and the FileExchangeView is updated to reflect the success (or otherwise) of the request:

            status = httpURLConnection.getResponseCode();//@@            message = httpURLConnection.getResponseMessage(); //@@          } catch (Exception e) {            e.printStackTrace();          }          //inform the application view          if (status >= HttpURLConnection.HTTP_OK ||              status < HttpURLConnection.HTTP_MULT_CHOICE) {            view.appendMessage("Delete:Success (" + status + ") " + message);          } else {            view.appendMessage("Delete:Error (" + status + ") " + message);            return;          }        }        //refresh the file list        refreshRequest();      } 

Here is where the application starts the process of sending an HTTP POST request to our servlet. This method is called each time we require a connection to the server. Using the URL object we open a connection to the servlet:

      private HttpURLConnection getHttpURLConnection(URL url)                                                     throws IOException {        //setup connection        HttpURLConnection httpURLConnection =                                      (HttpURLConnection) url.openConnection(); 

To use the connection for output we call the setDoOutput() method with true as input parameter, indicating we are using it for output. This triggers a POST request to be made, which means that the servlet's doPost() method will process this request. By calling the setUseCaches() method with false we are telling the connection that we do not want to use cached content if available. We also should set the Content-Type header to indicate the MIME type of the body of the request; in this case a Java serialized object (initially). Setting the Content-Type header tells the recipient of the request the type of data that we are sending. This is important, because the recipient servlet may be able to accept many types of requests and so it might need to know what type of data we are sending so that it can handle our request appropriately:

        httpURLConnection.setDoOutput(true);        httpURLConnection.setUseCaches(false);        httpURLConnection.setRequestProperty("Content-Type",                       "application/x-java-serialized-object");        return httpURLConnection;      } 

We have two sendRequest() methods overloaded. The first takes a connection and request object and, by default, closes the OutputStream. The second overloaded method takes an additional boolean argument to indicate if it should close the OutputStream. In any case using these methods set up the OutputStream and write the Request object out:

      private void sendRequest(HttpURLConnection httpURLConnection,                               BaseRequest request) throws IOException {        sendRequest(httpURLConnection, request, true);      } 

The application then has to get a reference to the OutputStream for the connection to be able to send data to the server, which is wrapped in an ObjectOutputStream to send our FileName request object. We then flush() the object to the server:

      private void sendRequest(HttpURLConnection httpURLConnection,          BaseRequest request, boolean closeStream) throws IOException {        //send the request query object to the server        OutputStream os = httpURLConnection.getOutputStream();        ObjectOutputStream oos = new ObjectOutputStream(os);        oos.writeObject(request);        oos.flush();        if (closeStream) {          os.close();        }      } 

The readResponse() method allows us to read back the server's response to a request. We access the connection's InputStream and wrap it in an ObjectInputStream, so that we can read back the serverResponse object. Two overloaded readResponse() methods are provided, one of which allows the caller to determine if the InputStream is closed:

      private Object readResponse(HttpURLConnection httpURLConnection)        throws IOException, ClassNotFoundException {          return readResponse(httpURLConnection, true);        }      private Object readResponse(HttpURLConnection httpURLConnection,        boolean closeOutput) throws IOException, ClassNotFoundException {        InputStream ins = httpURLConnection.getInputStream();        ObjectInputStream ois = new ObjectInputStream(ins);        Object response = ois.readObject();        if (closeOutput) {          ois.close();        }        return response;      }    } 

The FileExchangeFilter Class

The FileExchangeFilter class extends the FileFilter abstract class to provide the FileChooser with filtering ability based on our chosen criteria. We implement this class so that we can ensure that we only upload specified text/character files:

    package firewall.client;    import java.io.File;    import java.util.*;    import javax.swing.filechooser.*;    public class FileExchangeFilter extends FileFilter {      private Hashtable filters = null;      private String description = "Files"; 

The constructor takes an array (from common.FileValidator in our case) of String file extensions, and a description for them:

      public FileExchangeFilter(String[] extensions, String description) {        filters = new Hashtable();        for (int i = 0; i < extensions.length; i++) {          filters.put(extensions[i].toLowerCase(), this);        }        if(description!=null) {          this.description = description;        }      } 

Users of the FileFilter abstract class must override two methods; accept() and getDescription(). The accept method is used to determine if a file meets the specified file extension criteria:

      public boolean accept(File file) {        if(file != null) {          if(file.isDirectory()) {            return true;          }          String extension = getExtension(file);          if(extension != null && filters.get(getExtension(file)) != null) {            return true;          }        }        return false;      } 

Next we have a getExtension() helper method to return the file's extension:

      public String getExtension(File file) {        if(file != null) {          String filename = file.getName();          int dot = filename.lastIndexOf('.');          if(dot > 0 && dot < filename.length() - 1) {            return filename.substring(dot + 1).toLowerCase();          }        }        return null;      } 

Finally our getDescription() method returns the more friendly description of the filter:

      public String getDescription() {        return description;      }    } 

Implementing the Common Classes

The classes in this project consist of serializable classes used by the client and server to send information in client requests and server responses.

The BaseRequest Class

The BaseRequest class is the superclass for the other classes in this package. Objects of this class contain the requestType field indicating which type of request is in use:

      package firewall.common;      import java.io.Serializable;      public class BaseRequest implements Serializable { 

We have four constants defined for the different requests, and a String indicating the request type for the object of this class:

      public static final String UPLOAD = "UPLOAD";      public static final String DOWNLOAD = "DOWNLOAD";      public static final String REFRESH = "REFRESH";      public static final String DELETE = "DELETE";      protected String requestType; 

The constructor sets the requestType:

      public BaseRequest(String requestType) {        this.requestType = requestType;      } 

Finally there is the accessor method for requestType:

      public final String getRequestType() {        return requestType;      }    } 

The FileName Class

The FileName class extends the BaseRequest class to add a String field that holds a filename. We have get and set methods for this field:

    package firewall.common;    import java.io.Serializable;    public class FileName extends BaseRequest implements Serializable {      private String fileName;      public FileName(String requestType, String fileName) {        super(requestType);        this.fileName = fileName;      }      public String getFileName() {        return fileName;      }      public void setFileName(String fileName) {        this.fileName = fileName;      }    } 

The FileList Class

The FileList class extends the BaseRequest class to add a field, which holds a string array of filenames, with getter and setter methods for the object. The constructor doesn't yet require a request type since only one request type is used with this class, but it is a good idea to include it for flexibility, in case we extend the use of this class:

    package firewall.common;    import java.io.Serializable;    public class FileList extends BaseRequest implements Serializable {      String[] fileList;      public FileList(String requestType, String[] fileList) {        super(requestType);        this.fileList = fileList;      }      public String[] getFileList() {        return fileList;      }      public void setFileList(String[] fileList) {        this.fileList = fileList;      }    } 

The FileValidator Class

This class is a utility class to perform validation of filenames, and to 'clean' an array of filenames of names that do not meet the specified criteria. For a filename to be acceptable, it must have the extension of one of the Strings in the VALID_EXTENSIONS array:

    package firewall.common;    public class FileValidator {      public static final String[] VALID_EXTENSIONS =                                                {"txt", "html", "htm", "java"}; 

The isFileNameValid() method validates the filename passed in, subject to it not being null and having a specifed extension:

      public static boolean isFileNameValid(String filename) {        if (filename == null || filename.length() == 0) {          return false;        }        for (int i = 0; i < VALID_EXTENSIONS.length; i++) {          if (filename.endsWith("." + VALID_EXTENSIONS[i])) {            return true;          }        }        return false;      } 

The cleanFileList() method takes a reference to a String array of filenames and 'cleans' it of any filenames that do not have an acceptable extension:

      public static void cleanFileList(String[] files) {        for (int i = 0; i < files.length; i++) {          if (!isFileNameValid(files[i])) {            files[i] = null;          }        }      }    } 

Implementing the Web Application

The design issues facing the developer of the server side of this application are relatively easy to solve using the correct application of design patterns to the problem. The problem is that we have four request types being sent to the servlet in the current design, and possibly more in the future. Therefore we need to implement a design that will allow us to extend our application relatively easily.

The client application makes a request to the FileExchangeServlet. This servlet then calls the RequestHandlerFactory class, that implements the Singleton design pattern. The Singleton pattern is used to ensure that one and only one instance of a class can exist. To do this we have a static getInstance() method that on the first call creates the single instance, and then returns the singleton instance to the caller. The constructor is declared private so that only a static class method can create an instance (getInstance() in this case)). It also implements a Factory pattern, which means that it contains a mapping of all request types to their appropriate handlers, and can instantiate and return the correct handler object to process the request.

The handlers all extend from the abstract RequestHandler class, which declares the abstract respond() method to be overridden by all subclasses. Once the servlet has the appropriate handler object, it can call the setHttpObjects() method to prepare the handler and then the respond() method to process the request. In all cases the handler performs some task on the server directory (gets a file listing, deletion, up or download) and returns the response to the client.

This sequence of events is shown in the following diagram:

click to expand

The key advantage to using the Factory pattern is that we can easily add many more request types to the RequestHandlerFactory, and simply provide another handler class that extends from RequestHandler to process each new request type. This is a relatively painless way to make the application extensible; aside from the new handler subclass, the only other class affected would be the RequestHandlerFactory which needs a new mapping added.

The majority of the HTTP request processing is to be found in the four RequestHandler subclasses.

The FileExchangeServlet Class

This servlet handles the client requests, delegating processing to the appropriate handler through the use of the RequestHanderFactory:

    package firewall.server;    import firewall.common.*;    import javax.servlet.http.HttpServlet;    import java.io.*;    import javax.servlet.*;    import javax.servlet.http.*;    import java.io.IOException;    import java.io.PrintWriter;    import java.util.Enumeration;    public class FileExchangeServlet extends HttpServlet {      private RequestHandlerFactory requestHandlerFactory = null; 

The init() method sets up the directory location for files that the servlet makes accessible to the client. Normally it would be better to do this in the initialization parameters, so that we could easily configure their location without having to recompile. Chapter 4 will describe how we do this, but for now we code it here to %CATALINA_HOME%/WEB-INF/files/, which is a location that is not normally accessible to the client (everything below the WEB-INF directory is private to the web application). It then sets the files directory in the RequestHandler for future use:

      public void init() throws ServletException {        String directory = getServletContext().getRealPath("/") +                            "WEB-INF" + File.separator + "files" +                            File.separator ;        RequestHandler.setStringFileBase(directory); 

Next it initializes the RequestHandlerFactory object, by getting the servlet a reference to the singleton instance of this class:

        requestHandlerFactory = RequestHandlerFactory.getInstance();      } 

The doPost() method is a short method, but it packs a lot of processing behind the scenes by delegating the majority of the request processing to the handlers, which are created for each type of request and registered in the RequestHandlerFactory.

The method reads in the serialized object sent by the client, using the readSerializedObject() method. This object is cast into a BaseRequest object, which is the superclass for all the common communication objects. From the BaseRequest object we can determine the request type being made using the getRequestType() method of BaseRequest class. This is passed into the getHandler() method of the requestHandlerFactory, which performs all the identification and mapping of the request to the correct handler as well as instantiating the handler. The setHttpObjects() method is used to give the handler access to these objects during the processing of the request:

      public void doPost(HttpServletRequest request,                         HttpServletResponse response)                         throws ServletException, IOException {        try {          BaseRequest baseObject = (BaseRequest) readSerializedObject(request);          RequestHandler handler = (RequestHandler) requestHandlerFactory                                   .getHandler(baseObject.getRequestType());          handler.setHttpObjects(request, response); 

Finally, the respond() method is called in the handler that processes the request:

          handler.respond(baseObject);        } catch (ClassNotFoundException cnfe) {          cnfe.printStackTrace();        } catch (IOException ioe) {          ioe.printStackTrace();        }      } 

Currently, the doGet() method is not called because all requests from our application are POST requests, but we forward this method to the doPost() method so that we can incorporate additional request types using either HTTP request type. Our client's refresh method should probably use the GET request type for its request since it is a refresh request, and similarly the delete request could use the DELETE method, but for the sake of keeping the example straightforward we allow them to be POST requests too. From the servlet's point of view this makes no difference; it is just HTTP convention.

      public void doGet(HttpServletRequest request,                        HttpServletResponse response)                        throws ServletException, IOException {        doGet(request, response);      } 

We also have the readSerializedObject() method that reads the first object from the HTTPServletRequest InputStream. However, we do not close the InputStream, which leaves it available to read further input information if some of the requests need to. In fact, the only request type that does so is the upload request, where we will also have to read in the file uploaded to the server after this serialized object:

      private Object readSerializedObject(HttpServletRequest httpServletRequest)                                    throws IOException, ClassNotFoundException {        ObjectInputStream ois =          new ObjectInputStream(httpServletRequest.getInputStream());        Object readObject = ois.readObject();        return readObject;      }    } 

The RequestHandlerFactory Class

The RequestHandlerFactory is designed to manufacture RequestHandler subclass instances appropriate to the received request type. As mentioned previously, this class implements the singleton pattern and so only one instance of it per virtual machine can exist:

    package firewall.server;    import firewall.common.*;    import java.util.*;    public class RequestHandlerFactory { 

The following Map object stores a map of request types to RequestHandler subclasses:

      private Map requestHandlers; 

The static RequestHandlerFactory singleton object is the single instance of this class. The constructor is declared private to prevent instantiation of this class by any other class. We populate the Map with the request types and associated handler classes:

      private static RequestHandlerFactory singleton = null;      private RequestHandlerFactory() {        requestHandlers = new HashMap(5);        requestHandlers.put(BaseRequest.DELETE, DeleteHandler.class);        requestHandlers.put(BaseRequest.REFRESH, RefreshHandler.class);        requestHandlers.put(BaseRequest.DOWNLOAD, DownloadHandler.class);        requestHandlers.put(BaseRequest.UPLOAD, UploadHandler.class);      } 

The static getRequestHandlerFactory() method returns the singleton instance of the class. If it is not already created it will instantiate it:

      public static RequestHandlerFactory getInstance() {        if (singleton == null) {          synchronized (RequestHandlerFactory.class) {            if (singleton == null) {              singleton = new RequestHandlerFactory();            }          }        }        return singleton;      } 

The getHandler() method receives the name of the request type as a parameter, and retrieves the appropriate handler class from the Map. It then calls the instantiateHandler() method to create an object of this class to return:

      public RequestHandler getHandler(String name) {        //look up the handler class in the map and create a new instance        Class handlerClass = (Class) requestHandlers.get(name);        RequestHandler handler = instantiateHandler(handlerClass);        return handler;      } 

The above call to the instantiateHandler() method will produce an instantiated object of the class provided as a parameter:

      private RequestHandler instantiateHandler(Class handlerClass) {        RequestHandler handler = null;        if (handlerClass != null) {          try {            handler = (RequestHandler) handlerClass.newInstance();          } catch (InstantiationException e) {            e.printStackTrace();          } catch (IllegalAccessException e) {            e.printStackTrace();          }        } 

In the event of problems with instantiating the class, we probably should provide a default error handler to process the request and return an error gracefully (or recover from the cause of the problem). For simplicity we return null, which the calling class can check:

        if (handler == null) {          //Code here could handle instantiation problems - perhaps with          //default handler just in case.        }        return handler;      }    } 

The RequestHandler Class

The RequestHandler class is the abstract parent class for the handler classes that process the different types of request. Extending from this class will allow a programmer to add a new request type to the web application, once they register it with the RequestHandlerFactory. The only method that must be implemented in a subclass of RequestHandler is the respond() method, which performs the bulk of the processing of the request:

    package firewall.server;    import javax.servlet.http.*;    import firewall.common.*;    import java.io.*;    public abstract class RequestHandler { 

To process the client's request, the handler must have access to the HttpServletRequest and HttpServletResponse objects:

      HttpServletRequest httpServletRequest;      HttpServletResponse httpServletResponse; 

The following String and File fields refer to the specified directory for files that is made available to the client. The fileBase object is instantiated by the constructor, while the stringFileBase object holds the default local directory, but is set with a static method in the servlet's init() method, where it can discover the files directory location:

      static String stringFileBase = File.separator;      File fileBase; 

The getFileList() method will return an array of Strings representing the list of files in the fileBase directory. The getFile() method will return a File object corresponding to the supplied filename in the fileBase directory:

      protected String[] getFileList() {        String[] list = fileBase.list();        return list;      }      protected File getFile(String fileName) {        return new File(fileBase, fileName);      } 

The constructor establishes the Tomcat home directory, and determines the location of the directory made accessible to the application (that is, %CATALINA_HOME%/webapps/httpServlet/WEB-INF/files/):

      public RequestHandler() {        fileBase = new File(stringFileBase);      } 

The respond() method will perform the bulk of the processing, specific to the request, and must be overridden by subclassing handlers:

      public abstract void respond(BaseRequest baseRequest); 

The setHttpObjects() method gives the handler access to the client's Request object, and allows the handler to respond using the Response object:

      public void setHttpObjects(HttpServletRequest httpServletRequest,                                 HttpServletResponse httpServletResponse) {        this.httpServletRequest = httpServletRequest;        this.httpServletResponse = httpServletResponse;      } 

The sendFile() and sendSerializedObject() methods return a file and an object to the client respectively. As noted previously, the reader and writer objects are for character based text files:

      public void sendFile(File sendFile) throws IOException {        FileReader fileReader = new FileReader(sendFile);        PrintWriter out = httpServletResponse.getWriter();        char c[] = new char[4096];        int read = 0;        //Read until end of file and send to client        while ((read = fileReader.read(c)) != -1) {          out.write(c, 0, read);        }        //Close        fileReader.close();        out.close();      }      public void sendSerializedObject(Object sendObject) throws IOException {        OutputStream os = httpServletResponse.getOutputStream();        ObjectOutputStream oos = new ObjectOutputStream(os);        oos.writeObject(sendObject);        oos.flush();        oos.close();      } 

We have two methods for setting HTTP status codes. SetStatusCode() simply sets the status code, while sendError() sends an error code with an accompanying message:

      public void setStatusCode(int statusCode) {        httpServletResponse.setStatus(statusCode);      }      public void sendError(int statusCode, String message)        throws IOException {        httpServletResponse.sendError(statusCode, message);      } 

The stringFileBase object was declared static so that the directory location could be set for all instances. Hence we have provided a static setStringFileBase() method to set this:

      public static void setStringFileBase(String fileBase) {        stringFileBase = fileBase;      }    } 

The RefreshHandler Class

RefreshHandler is a subclass of RequestHandler that refreshes the list of files for the client:

    package firewall.server;    import javax.servlet.http.HttpServletRequest;    import javax.servlet.http.HttpServletResponse;    import firewall.common.*;    import java.io.*;    public class RefreshHandler extends RequestHandler { 

The handler gets an up-to-date array of String objects representing the files in the server's directory, checks their validity, and returns it to the client (or an error if the FileList object is null):

      public void respond(BaseRequest baseRequest) {        FileList fileList = new FileList(BaseRequest.REFRESH, getFileList());        try {          if (fileList != null) {            setStatusCode(HttpServletResponse.SC_OK);            FileValidator.cleanFileList(fileList.getFileList());            sendSerializedObject(fileList);          } else {            sendError(HttpServletResponse.SC_NOT_FOUND, "Directory not found");          }        } catch (IOException e) {          e.printStackTrace();        }      }    } 

The DownloadHandler Class

This class retrieves and sends files back to the client that the client has requested:

    package firewall.server;    import javax.servlet.http.HttpServletRequest;    import javax.servlet.http.HttpServletResponse;    import firewall.common.*;    import java.io.*;    public class DownloadHandler extends RequestHandler { 

The respond() method in this class retrieves a File object for the requested file, and then uses the sendFile() method to send the file over to the client. We send an error code if the file does not exist, or if the filename supplied was invalid:

      public void respond(BaseRequest baseRequest) {        String fileName = ((FileName) baseRequest).getFileName();        try {          if (!FileValidator.isFileNameValid(fileName)) {            sendError(HttpServletResponse.SC_PRECONDITION_FAILED,                      "Filename is invalid");            return;          }          File fileToRead = new File(fileBase, fileName);          if (fileToRead.exists()) {            setStatusCode(HttpServletResponse.SC_OK);            sendFile(fileToRead);          } else {            sendError(HttpServletResponse.SC_NOT_FOUND, "File not found");          }        } catch (IOException e) {          e.printStackTrace();        }      }    } 

The UploadHandler Class

This class processes the uploading of files to the server's files directory:

    package firewall.server;    import javax.servlet.http.*;    import firewall.common.*;    import java.io.*;    public class UploadHandler extends RequestHandler { 

Next we get the InputStreamReader to read in the file from the client, and use a FileWriter to write the file to the server. The file being sent must have a valid name. Then an HTTP status code is returned to indicate that the file was created on the server:

      public void respond(BaseRequest baseRequest) {        try {          InputStreamReader isr =            new InputStreamReader(httpServletRequest.getInputStream());          BufferedReader bfr = new BufferedReader(isr);          String fileName = ((FileName) baseRequest).getFileName();          if (!FileValidator.isFileNameValid(fileName)) {            sendError(HttpServletResponse.SC_PRECONDITION_FAILED,                     "Filename is invalid");            return;          }          File newFile = new File(fileBase, fileName);          FileWriter fileWriter = new FileWriter(newFile);          char c[] = new char[4096];          int read = 0;          while ((read = bfr.read(c)) != -1) {            fileWriter.write(c, 0, read);          }          fileWriter.close();          isr.close();          setStatusCode(HttpServletResponse.SC_CREATED);        } 

Finally, if an error occurs during IO, we send an error to the client if possible:

        catch (IOException ioe) {          try {            sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,                      "File not uploaded");          } catch (IOException ioe2) {            ioe2.printStackTrace();          }        }      }    } 

The DeleteHandler Class

This class processes a request from the client to delete a named file from the server's files directory:

    package firewall.server;    import javax.servlet.http.HttpServletRequest;    import javax.servlet.http.HttpServletResponse;    import firewall.common.*;    import java.io.*;    public class DeleteHandler extends RequestHandler { 

A new File object is created from the filename sent by the client and the delete() method is called to remove it. If the file is successfully deleted the response to the client indicates this; otherwise it indicates to the client that the file was not found or if the filename was invalid, using HTTP status codes:

      public void respond(BaseRequest baseRequest) {        String fileName = ((FileName) baseRequest).getFileName();        try {          if (!FileValidator.isFileNameValid(fileName)) {            sendError(HttpServletResponse.SC_PRECONDITION_FAILED,                      "Filename is invalid");            return;          }          File fileToDelete = new File(fileBase, fileName);          boolean result = fileToDelete.delete();          if (result) {            setStatusCode(HttpServletResponse.SC_NO_CONTENT);          } else {            sendError(HttpServletResponse.SC_NOT_FOUND, "File not deleted");          }        } catch (IOException e) {          e.printStackTrace();        }      }    } 

Compiling and Running the Application

There are a number of steps that you will need to take to set up the application. If you downloaded the code from the http://www.apress.com website you can probably skip over a few steps, but I'll assume that you are creating the web application from the start.

First, add the following subdirectories to the httpServlet/WEB-INF/src and classes directories:

  • firewall/client

  • firewall/server

  • firewall/common

Obviously these directories will contain the source code for the corresponding packages described above. When you have added the source code to the appropriate directories, compile the code in each directory/package from the src directory, starting with the common package. Then move the classes into their corresponding subdirectories in the classes directory.

Then create a files directory under httpServlet/WEB-INF, and add a few text files to it for exchange with the servlet-application.

Next we need to JAR up the client application. First, add a Manifest.MF text file to the classes directory with the following text:

    Manifest-Version: 1.0    Main-class: firewall.client.FileExchangeApplication 

Then execute the following command from the classes directory to create the JAR:

    jar cvfm FileExchangeApplication.jar manifest.mf                                  firewall/common/* firewall/client/* 

Finally, restart Tomcat, and then execute the JAR using the following command, which runs the client application:

    java -jar FileExchangeApplication.jar "http://localhost:8080" 

At this point you should see the File Exchange Application window pop up, and you can play with the application.



 < Free Open Study > 



Professional Java Servlets 2.3
Professional Java Servlets 2.3
ISBN: 186100561X
EAN: 2147483647
Year: 2006
Pages: 130

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