HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML
 

With the Internet widely available, many software clients are asking for built-in networking capabilities. Data exchange is an advantage in any software product, and software that offers networking functions has a better chance of being selected.

Although the Internet has spread throughout the world, the rate of data transmission is still a problem, even on your business's local area network. Getting files from the Internet easily takes a few minutes even with a high-speed connection. Users want to see what's going on with their software when it's looking for data on the Internet. Telling them when it's over isn't sufficient. Programmers need to show them, by any means possible, that data is flowing between the server and their software. Elements such as the completion percentage, transfer rate, progress bar, estimated remaining download time and animation will make users wait more patiently in front of their screen. No more "Please wait..." messages.

I'll propose two classes that'll enable you to provide the details your clients are asking for. Your data transfer won't be faster, but users will be more likely to wait until the end rather than click the "Cancel" button in desperation.

I/O Handling
Before going into implementation details, I'll do a brief overview of I/O handling in Java. As you probably know, there are two I/O families: the input and output streams and the reader/writers.

Streams are low-level classes that allow you to directly use the disk, networking and peripherals to read and write raw bytes. Readers and writers are wrapper classes that translate unformatted data provided by the streams in data types, thus allowing them to be handled by programmers (boolean, int, float, string, even serialized objects). Since I'm willing to provide a source-dependent solution, I'd implement streams as base classes rather than readers and writers because they're too specific to be used in this situation.

There are more than 20 classes for handling data I/O in the stream mode, but they all have something in common: they inherit from the java.io.InputStream or java.io.OutputStream classes. Studying these classes, I've found that only two of their methods are abstract: the read() method (from the InputStream) and the write() method (from the OutputStream). All I had to do was implement these methods and add new ones.

The Architecture Used
I'll create wrapper classes that will plug into other classes to add functions to the old ones, the same way reader and writer classes do. See Figure 1 for the class hierarchy diagram.

Figure 1
Figure 1:

The inheritance technique isn't usable because in order to be source-dependent, the solution can't be as specific as FileInputStream, SocketInputStream and all the other classes. It's simply impossible to extend all these classes. Rather, I'd extend the abstract InputStream (and OutputStream) to create wrapper classes.

Since my classes will extend InputStream (or OutputStream), they can be used in the same way as any base class, even in conjunction with reader/writer classes. The main goal is to provide programmers with a class that acts the same way as the InputStream and OutputStream classes.

Figures 2A and B give examples of possible uses of my classes: binary reading from a URL and formatted reading from a socket connection.

Figure 2A
Figure 2A:
Figure 2B
Figure 2B:

Defining the Problem
Which variables should you show to your users to make their wait a little easier? Here's a short list of data users want to see:

  • Number of bytes read
  • Total size
  • Elapsed downloading or uploading time
  • Mean transfer rate
  • Estimated remaining download or upload time
To provide your users with this data, you need only a few variables - start time, number of bytes read and total size. With these variables you'll be able to compute any of the values listed above. It must be said, however, that some of these variables aren't always available. While downloading data from a servlet, for example, you won't know the full length of what you're downloading until it's over (and that's too late). This is also the case for most custom protocols over a socket connection.

Implementing the Classes
Since these classes must be able to be used from any source, they must inherit from java.io.InputStream and java.io.OutputStream. As I've said before, it's impossible to know the exact length of an InputStream or an OutputStream. This leaves us with two possibilities: force the developer to give it through a constructor, which allows us to provide needed values, or simply ignore it, in which case some values won't be available. The only exception to this problem is the java.net.URL object. With the java.net.URLConnection class, the developer will usually know the size of a download before reading it. To simplify the process, a special constructor has been made to handle URLs.

Here are the constructors for both wrapper classes:

  • DownloadInputStream(InputStream is, int size)
  • DownloadInputStream(InputStream is)
  • DownloadInputStream(URL url)
  • UploadOutputStream(OutputStream os, int size)
  • UploadOutputStream(OutputStream os)
Almost every method call is simply a call to its equivalent on the private internal instance. The available() method of DownloadInputStream calls the available() method on the private instance of the InputStream given through the constructor.

public int available() throws IOException {
return internalInputStream.available();
}

You need to know the starting time and the number of bytes being read up to now. To provide the user with this data, you need to reimplement the read(...) and write(...) methods so that the information is computed as the stream is being used. The following code shows the reimplementation of the read(...) method of the DownloadInputStream.

public int read(byte[] b, int off, int len)
throws IOException {
if (startTime == -1)
startTime = System.currentTimeMillis();

int read = internalInputStream.read(b,off,len);
bytesRead += read;
return read;
}

All you have to do is implement new methods that'll compute all the information you need to know about these streams with the new variables.

The transfer rate is quite simple - divide the number of bytes being read by the elapsed download time. The result is in bytes per millisecond. See Listing 1 for the exact implementation of this method.

The estimated remaining download or upload time is computed with an interpolation that relies on the transfer rate you've just computed. The hypothesis is that the rest of the data to be transferred will be downloaded/uploaded at approximately the same rate as the first part. The -1 value is returned if the full length of the input or output stream is unknown. The exact implementation of this method is shown in Listing 2.

Example
Finally, since a bit of code is worth a thousand words, I've provided an example to prove how simple it can be to use these classes. It's a short program that downloads a given file from a URL on the local disk. While the file is being downloaded, data such as transfer rate, estimated remaining download time and a progress bar will show the user the state of his or her download (see Figure 3). See Listing 3 for the full example code.

Figure 3

Limitations
Keep in mind that the data provided by these classes must be carefully interpreted. If you build a program that calls the read(...) method every five minutes, you won't get the exact transfer rate and estimated download time whether you're using a 100Mb LAN or a dial-up modem. Since the interpolation is really simple, it can't handle extreme cases such as this.

With this in mind, you'll now be able to use these classes to provide your users with all the data they want to know. Since these classes can be used with the same methods as the common Java Input and Output Streams, you won't have to go through a learning curve.

Author Bio
Alexandre Lemieux is studying computer engineering at the Universite du Quebec ˆ Chicoutimi in Canada. He also teaches UNIX, PERL and Java at the Humanis, Centre de Formation Continue, and manages a software development project for Humanis using Java servlets and distributed objects with RMI.
He can be reached at: [email protected].

	

Listing 1: 

package net.fortrel.io; 

import java.io.*; 
import java.net.*; 

/** 
 * This is the Wrapper class for the Input 
 * Stream. 
 */ 
public class DownloadInputStream 
      extends InputStream { 

   // The Input Stream from which to read. 
   private InputStream internalInputStream; 
  
   // The length of the input stream, the 
   // starting time of download and the number 
   // of bytes being read. 
   private int length = -1; 
   private long startTime = -1; 

 private long bytesRead = 0; 
  
   /** 
    * Constructor if we know the length of the 
    * input stream. 
    */ 
   public DownloadInputStream(Input 
   Stream is, 
                        int length) { 
      internalInputStream = is; 
      this.length = length; 
   } 
  
   /** 
    * Constructor if the length is unknown. 
    */ 
   public DownloadInputStream(Input 
   Stream is) { 
      internalInputStream = is; 
   } 

   /** 
    * Special constructor for a URL. 
    */ 
   public DownloadInputStream(URL url) 
         throws IOException { 
      URLConnection connection = 
         url.openConnection(); 
      this.length = connection.getContentLength(); 
  
      internalInputStream = 
         connection.getInputStream(); 
   } 

   /** 
    * Get the current download rate in bytes 
    * per millisecond. 
    */ 
   public float getDownloadRate() { 
      if (startTime != -1) { 
         long currentTime = 
            System.currentTimeMillis(); 
  
         return bytesRead / (float)(currentTime - 
            startTime); 
      } 
      else 
         return -1.0f; 
   } 

   /** 
    * Get the time since the beginning of the 
    * download in milliseconds. 
    */ 
   public long getDownloadTime() { 
      if (startTime != -1) 
         return System.currentTimeMillis() - 
            startTime; 
      else 
         return 0; 
   } 
  
   /** 
    * Get the completion percent. 
    */ 
   public float getDownloadedPercent() { 
      if (startTime == -1 || length == -1) 
         return -1.0f; 
      else { 
         return (float)bytesRead / (float)length; 
      } 
   } 
  
   /** 
    * Get the number of bytes read until now. 
    */ 
   public long getBytesRead() { 
      return bytesRead; 
   } 

   /** 
    * Get the length of the input stream. 
    * Returns -1 if the length is unknown. 
    */ 
   public long getBytesTotal() { 
      return length; 
   } 

   /** 
    * Get the estimated remaining download 
    * time in milliseconds. 
    */ 
   public long getEstimatedDownloadTime() { 
      if (startTime == -1 || length == -1) 
         return -1; 
  
      long currentTime = 
         System.currentTimeMillis(); 
  
      return (long)((length - bytesRead) / 
         getDownloadRate()); 
   } 
 /*************************************** 
 * All other methods are the implemen- 
 * tation of the InputStream methods.    ***************************************/ 
  
   public int available() throws IOException { 
      return internalInputStream.available(); 
   } 

   public void close() throws IOException { 
      internalInputStream.close(); 
   } 

   public void mark(int readlimit) { 
      internalInputStream.mark(readlimit); 
   } 

   public boolean markSupported() { 
      return internalInputStream.markSupported(); 
   } 
  
   public int read() throws IOException { 
      if (startTime == -1) 
         startTime = System.currentTimeMillis(); 
  
      int b = internalInputStream.read(); 
  
      if (b != -1) 
         bytesRead++; 

      return b; 
   } 

   public int read(byte[] b, int off, int len) 
         throws IOException { 
      if (startTime == -1) 
         startTime = System.current- 
         TimeMillis(); 
  
      int read = internalInput- 
      Stream.read(b,off, 
                   len); 
  
      bytesRead += read; 
  
      return read; 
   } 

   public void reset() throws IOException { 
      startTime = -1; 
      bytesRead = 0; 
  
      internalInputStream.reset(); 
   } 

   public long skip(long n) throws 
   IOException { 
      long skipped = internalInput- 
      Stream.skip(n); 
      bytesRead += skipped; 
  
      return skipped; 
   } 
} 

Listing 2: 

package net.fortrel.io; 

import java.io.*; 

/** 
 * This is the Wrapper class for the 
 * Output Stream. 
 */ 
public class UploadOutputStream 
      extends OutputStream { 
  
   // Private internal stream. 
   private OutputStream internalOutputStream; 
  
   // The length of the output stream, the 
   // upload start time and the number of 
   // bytes being writen. 
   private int length=-1; 
   private long startTime=-1; 
   private int bytesWriten=0; 
  
   /** 
    * Constructor to use if the length is 
    * unknown. 
    */ 
   public UploadOutputStream(Output- 
   Stream os) { 
      internalOutputStream = os; 
   } 
  
   /** 
    * Constructor to use when the 
    * length is known. 
    */ 
   public UploadOutputStream(Output- 
   Stream os, 
                        int length) { 
      internalOutputStream = os; 
      this.length = length; 
   } 
  
   /** 
    * Get the upload transfer rate in 
    * bytes per millisecond. 
    */ 
   public float getUploadRate() { 
      if (startTime != -1) { 
         long currentTime = 
            System.currentTimeMillis(); 
  
         return bytesWriten / 
            (float)(currentTime-startTime); 
      } 
      else 
         return -1.0f; 
   } 

   /** 
    * Get the time since the beginning 
    * of the upload in milliseconds. 
    */ 
   public long getUploadTime() { 
      if (startTime != -1) 
         return System.currentTimeMillis() - 
             startTime; 
      else 
         return -0; 
   } 

   /** 
    * Get the completion percent. 
    */ 
   public float getUploadPercent() { 
      if (startTime == -1 || length == -1) 
    return -1.0f; 
      else { 
         return (float)bytesWriten / 
            (float)length; 
      } 
   } 

   /** 
    * Get the number of bytes being 
    * writen by now. 
    */ 
   public long getBytesWriten() { 
      return bytesWriten; 
   } 

   /** 
    * Get the length of the output 
    * stream content to be writen. 
    */ 
   public long getBytesTotal() { 
      return length; 
   } 

   /** 
    * Get the estimated uploading time 
    * in milliseconds. 
    */ 
   public long getEstimatedUploadTime() { 
      if (startTime == -1 || length == -1) 
         return -1; 
  
      long currentTime = 
         System.currentTimeMillis(); 
  
      return (long)((length - 
      bytesWriten) / 
         getUploadRate()); 
   } 
 /*************************************** 
    * All other methods are the imple- 
    * mentation of the OuputStream methods.   ***************************************/ 
   public void close() throws IOException { 
      internalOutputStream.close(); 
   } 
  
   public void flush() throws IOException { 
      internalOutputStream.flush(); 
   } 
  
   public void write(byte[] b, int off, int len) 
         throws IOException { 
      if (startTime == -1) 
         startTime = System.current- 
         TimeMillis(); 
  
      bytesWriten += len; 
              internalOutputStream.write(b,off,len); 
   } 
  
   public void write(int b) throws 
   IOException { 
      if (startTime == -1) 
         startTime = System.current- 
         TimeMillis(); 
      bytesWriten++; 
  
      internalOutputStream.write(b); 
   } 
} 

Listing 3: 

package net.fortrel.io; 

import java.io.*; 
import java.net.*; 
import java.awt.*; 
import javax.swing.*; 

/** 
 * An example on using the DownloadIn- 
 * putStream. 
 */ 
public class DownloadDialog extends JDialog 
      implements Runnable { 
   private JProgressBar bar; 
   private JLabel lTransfertRate, 
               lEstimatedDownloadTime, 
               lBytesRead, 
               lBytesTotal; 
  
   private DownloadInputStream dis; 
   private OutputStream os; 

   private Thread myThread; 

   public DownloadDialog(Frame owner, 
                       URL url, 
                       String outputFile) 
         throws IOException { 
      super(owner, "Download", false); 

      // Build the User Interface. 
      buildUI(url.toString()); 
      pack(); 
  
      // Open every streams. 
      dis = new DownloadInputStream(url); 
      os = new FileOutputStream(outputFile); 
  
      // Start the downloading thread. 
      myThread = new Thread(this); 
      myThread.start(); 
   } 

   /** 
    * A private method to build the 
    * User Interface. 
    */ 
   private void buildUI(String url) { 
      JLabel lFile = new 
      JLabel("File: " + url); 
      lBytesRead = new JLabel("Bytes 
      read:"); 
      lBytesTotal = new 
      JLabel("Size:"); 
      lTransfertRate = new 
         JLabel("Transfer rate:"); 
      lEstimatedDownloadTime = new 
         JLabel("Estimated downloading 
         time:"); 
      bar = new JProgressBar( 
                 JProgressBar.HORIZON 
                 TAL, 0, 100); 

      // Do the layout. 
      getContentPane().setLayout(new 
      GridLayout(0,1)); 
  
      getContentPane().add(lFile); 
      getContentPane().add(lBytesRead); 
      getContentPane().add(lBytesTotal); 
      getContentPane().add(lTransfertRate); 
      getContentPane().add(lEstimated- 
      DownloadTime); 
      getContentPane().add(bar); 
   } 
  
   /** 
    * The downloading thread method. 
    */ 
   public void run() { 
      byte buffer[] = new byte[1024]; 
  
      while(true) { 
         try { 
            int b = dis.read(buffer); 
  
            if (b <= 0) { 
               // The end of the down- 
               // load is reached. 
               os.close(); 
  
               System.exit(0); 
            } 
            else { 
               // Save the data 
               os.write(buffer, 0, b); 
  
               // Update the on-screen 
               // fields. 
               lBytesRead.setText("Bytes read:" + 
                  dis.getBytesRead()); 
               lBytesTotal.setText("Size:" + 
                  dis.getBytesTotal()); 
               lTransfertRate.setText( 
                  "Transfer rate:" + 
                  dis.getDownloadRate() * 
                  (1000f/1024f) + 
                  " kb/s"); 
               lEstimatedDownloadTime.setText( 
                  "Estimated downlaod- 
                   ing time:" + 
                  (int)(dis.getEsti- 
                  matedDownloadTime() 
                  / 1000) + " sec."); 
               bar.setValue( 
                  (int)(dis.getDown- 
                  loadedPercent() * 
                  100)); 
            } 
         } 
         catch(IOException e) { 
            System.err.println("Error: " + e); 
            break; 
         } 
      } 
   } 

   /** 
    * The main method. 
    * Use two parameters: the URL and 
    * the filename to create. 
    */ 
   public static void main(String   args[]) { 
      if (args.length != 2) { 
         System.err.println("Please 
         give the URL"+ 
            " to connect to and the 
              file store"+ 
            " the downloaded data."); 
         System.exit(-1); 
      } 
  
      try { 
         URL url = new URL(args[0]); 
         String filename = args[1]; 
  
         DownloadDialog dd = 
            new DownloadDialog(new 
            JFrame(), 
                           url, 
                           filename); 
  
         dd.show(); 
      } 
      catch(Exception e) { 
         System.err.println("Error:" + e); 
  
         e.printStackTrace(); 
      } 
   } 
} 



 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.