Monday, September 5, 2011

Building Your Own Download Manager

Well it always gives a new thrill to make something from scratch. It makes us feel like as if we are inventor.
Well, but, before that we need to work hard. We need to learn from examples around us. And keep learning.
Here is a Download manager with Pause and Resume capability. Read the code carefully and try to customize it or redesign it. I'm sure it'll always give you thrill .


import java.io.*;
import java.net.*;
import java.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;

// This class downloads a file from a URL.
class Download extends Observable implements Runnable {
  // Max size of download buffer.
  private static final int MAX_BUFFER_SIZE = 1024;

  // These are the status names.
  public static final String STATUSES[] = {"Downloading",
    "Paused", "Complete", "Cancelled", "Error"};

  // These are the status codes.
  public static final int DOWNLOADING = 0;
  public static final int PAUSED = 1;
  public static final int COMPLETE = 2;
  public static final int CANCELLED = 3;
  public static final int ERROR = 4;
//Following are download variables
  private URL url; // download URL
  private int size; // size of download in bytes
  private int downloaded; // number of bytes downloaded
  private int status; // current status of download

  // Constructor for Download.
  public Download(URL url)//Download's constructor is passed a reference to the URL to download in form of URL object,
  {
    this.url = url;//which is assigned to URL instance variable
    size = -1;//It sets the remaining instance variable to their initial states
    downloaded = 0;//and calls download() method.Notice that Size is set to -1 o indicate their is no Size yet
    status = DOWNLOADING;

    // Begin the download.
    download();
  }

  // Get this download's URL.
  public String getUrl() {
    return url.toString();
  }

  // Get this download's size.
  public int getSize() {
    return size;
  }

  // Get this download's progress.
  public float getProgress() {
    return ((float) downloaded / size) * 100;
  }

  // Get this download's status.
  public int getStatus() {
    return status;
  }

  // Pause this download.
  public void pause() {
    status = PAUSED;
    stateChanged();
  }

  // Resume this download.
  public void resume() {
    status = DOWNLOADING;
    stateChanged();
    download();
  }

  // Cancel this download.
  public void cancel() {
    status = CANCELLED;
    stateChanged();
  }

  // Mark this download as having an error.
  private void error() {
    status = ERROR;
    stateChanged();
  }
//download() method creates new Thread object, passing it a reference to the invoking Download instance. As mentioned before it's necessary
  //for each download to run independently. In order for download class to act alone,it must execute in its own thread.
  //
  // Start or resume downloading.
  private void download() {
    Thread thread = new Thread(this);
    thread.start();
  }

  // Get file name portion of URL.
  private String getFileName(URL url) {
    String fileName = url.getFile();
    return fileName.substring(fileName.lastIndexOf('/') + 1);
  }

  // Download file.
  public void run() {//when run method executes actual downloading goes under way.
    RandomAccessFile file = null;//First run sets up variables for the network stream that the download's content will be read from
    InputStream stream = null;//and sets up the file that the download's contents will be written to.

    try {
      // Open connection to URL.
      HttpURLConnection connection =                //next a connection to download's url is opened by url.openConnection().Since we know that download
        (HttpURLConnection) url.openConnection();  //manager supports only HTTP downloads, the connection is cast to the HttpURLConnection type
      //specify what portion of file to download
      connection.setRequestProperty("Range",      //casting the connection as a HttpURLConnection allows us to take advantages of HTTP-specific connection features such as getResponseCode()
              "bytes=" + downloaded + "-");      // Specify what portion of file to download.
                                                //NOTE: calling url.openConnection() method doesn't creates a actual connection to the URL's server
                                               //it simply creates a new URLConnection instance associated with the URL that later will be used to connect
                                              //to the server. After HttpURLConnection has been created, the connection request property is set by calling connection.setRequestProperty()
 //Setting request property allows extra reuqest information to be sent to the server the download will be running from. In this case, the "Range" property is set
   //This is critically important, as the range property specifies the range of bytes that are being requested for download from server.
     //Normally all of file's bytes are downloaded at once.However if download is interrupted or paused, only the download's remaining bytes
      //should be retrieved.Setting the range property is foundation of the Download's manager's operation.The range property is specified in
        //this form: start-byte-end-byte.
         //The end byte of range is optional.If the end byte is absent, the range ends at the end of the file.
          //The run() method never specifies the end byte because downloads must run until the entire range s downloaded unless paused or interrupted.
     
      // Connect to server.
      connection.connect();//This method is called to make actual connection to the download;s server.
                                                          
      // Make sure response code is in the 200 range.
      if (connection.getResponseCode() / 100 != 2) {//Response code returned by server is checked .
        error();                                   //NOTE: HTTP protocol has a list of response codes that indicates a server's response to a request. HTTP response codes are organized into
      }                              //into numeric ranges to 100, and 200 range indicates success. The servers response code is validated for being in the 200 range by calling connection.getResponseCode()
                                        //and dividing by 100. If the value of this divison is 2, the connection was successful.
      // Check for valid content length.
      int contentLength = connection.getContentLength();//run() gets the content Length by calling connection.getContentLength()
      if (contentLength < 1) {                         //The contentLength represents the number of bytes in the requested file.If content Length is <1, the
        error();  //error() method is called. The error() method updates the download's status to ERROR, and then calls stateChanged().
      }

     
      /* Set the size for this download if it
         hasn't been already set. */
      if (size == -1) {    //after getting the content length following code see's if it has already been assigned to the size variable:
        size = contentLength;  //As you see instead of assigning content length to size variable unconditionally, it only gets assigned if it hasn't already been given value. The reason for this is because content length reflects how mnay bytes server will be sending.
        stateChanged(); //if anything other than a 0-based start range is specified, the content length will only represent a portion of the file's size. The size variable has to be set to the complete size of the download's file
      }

      // Open file and seek to the end of it.
      file = new RandomAccessFile(getFileName(url), "rw");   //it's recieved with a call to getFileName() method.    
      file.seek(downloaded);// The RandomAccessFile is opened in "rw" mode, which specifies that file can be written to and read from.
                             //Once the file is open,run() seeks to end of the file by calling the file.seek() method, passing in downloaded variable.
                             //This tells the file to position itself as number of bytes that have been downloaded-in other words,at the end. It's necessary to position file at the end.It's necessary to position file at the end
                             //in case download is resumed.If the download is resumed newly downloaded bytes are appended to file and the dont overwrite any previously downloaded bytes.
     
     
     
      stream = connection.getInputStream();//After preparing the output file, the network stream handle to the open server connection
                                           //is obtained by calling connection.getInputStream()
     
      while (status == DOWNLOADING) {
        /* Size buffer according to how much of the
           file is left to download. */
        byte buffer[];
        if (size - downloaded > MAX_BUFFER_SIZE) {
          buffer = new byte[MAX_BUFFER_SIZE];
        } else {
          buffer = new byte[size - downloaded];
        }

        // Read from server into buffer.
        int read = stream.read(buffer);
        if (read == -1)
          break;

        // Write buffer to file.
        file.write(buffer, 0, read);
        downloaded += read;
        stateChanged();
      }

      /* Change status to complete if this point was
         reached because downloading has finished. */
      if (status == DOWNLOADING) {
        status = COMPLETE;
        stateChanged();
      }
    } catch (Exception e)
    {System.out.println("Error during download process: "+e);
      error();
    } finally {
      // Close file.
      if (file != null) {   //Finally block ensures that if the file and stream connections have been opened, they get closed whether
        try {                //an exception has been thrown or not
          file.close();
        } catch (Exception e) {System.out.println("Error while accessing file:"+e);}
      }

      // Close connection to server.
      if (stream != null) {
        try {
          stream.close();
        } catch (Exception e) {System.out.println("Download file is empty:"+e);}
      }
    }
  }

  // Notify observers that this download's status has changed.
  private void stateChanged() {
    setChanged();
    notifyObservers();
  }
}
/***********************************************************************************************************************************
 * *********************************************************************************************************************************
 */

//The Download Manager.
public class DownloadManager extends JFrame
implements Observer
{
/**
     *
     */
    private static final long serialVersionUID = 1L;

// Add download text field.
private JTextField addTextField;

// Download table's data model.
private DownloadsTableModel tableModel;

// Table listing downloads.
private JTable table;

// These are the buttons for managing the selected download.
private JButton pauseButton, resumeButton;
private JButton cancelButton, clearButton;

// Currently selected download.
private Download selectedDownload;

// Flag for whether or not table selection is being cleared.
private boolean clearing;

// Constructor for Download Manager.
public DownloadManager()
{
 // Set application title.
 setTitle("DownloadManager:A Alexander & Bros. corp. Production");

 // Set window size.
 setSize(640, 480);

 // Handle window closing events.
 addWindowListener(new WindowAdapter() {
   public void windowClosing(WindowEvent e) {
     actionExit();
   }
 });

 // Set up file menu.
 JMenuBar menuBar = new JMenuBar();
 JMenu fileMenu = new JMenu("File");
 fileMenu.setMnemonic(KeyEvent.VK_F);
 JMenuItem fileExitMenuItem = new JMenuItem("Exit",
   KeyEvent.VK_X);
 fileExitMenuItem.addActionListener(new ActionListener() {
   public void actionPerformed(ActionEvent e) {
     actionExit();
   }
 });
 fileMenu.add(fileExitMenuItem);
 menuBar.add(fileMenu);
 setJMenuBar(menuBar);

 // Set up add panel.
 JPanel addPanel = new JPanel();
 addTextField = new JTextField(30);
 addPanel.add(addTextField);
 JButton addButton = new JButton("Add Download");
 addButton.addActionListener(new ActionListener() {
   public void actionPerformed(ActionEvent e) {
     actionAdd();
   }
 });
 addPanel.add(addButton);

 // Set up Downloads table.
 tableModel = new DownloadsTableModel();
 table = new JTable(tableModel);
 table.getSelectionModel().addListSelectionListener(new
   ListSelectionListener() {
   public void valueChanged(ListSelectionEvent e) {
     tableSelectionChanged();
   }
 });
 // Allow only one row at a time to be selected.
 table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

 // Set up ProgressBar as renderer for progress column.
 ProgressRenderer renderer = new ProgressRenderer(0, 100);
 renderer.setStringPainted(true); // show progress text
 table.setDefaultRenderer(JProgressBar.class, renderer);

 // Set table's row height large enough to fit JProgressBar.
 table.setRowHeight(
   (int) renderer.getPreferredSize().getHeight());

 // Set up downloads panel.
 JPanel downloadsPanel = new JPanel();
 downloadsPanel.setBorder(
   BorderFactory.createTitledBorder("Downloads"));
 downloadsPanel.setLayout(new BorderLayout());
 downloadsPanel.add(new JScrollPane(table),
   BorderLayout.CENTER);

 // Set up buttons panel.
 JPanel buttonsPanel = new JPanel();
 pauseButton = new JButton("Pause");
 pauseButton.addActionListener(new ActionListener() {
   public void actionPerformed(ActionEvent e) {
     actionPause();
   }
 });
 pauseButton.setEnabled(false);
 buttonsPanel.add(pauseButton);
 resumeButton = new JButton("Resume");
 resumeButton.addActionListener(new ActionListener() {
   public void actionPerformed(ActionEvent e) {
     actionResume();
   }
 });
 resumeButton.setEnabled(false);
 buttonsPanel.add(resumeButton);
 cancelButton = new JButton("Cancel");
 cancelButton.addActionListener(new ActionListener() {
   public void actionPerformed(ActionEvent e) {
     actionCancel();
   }
 });
 cancelButton.setEnabled(false);
 buttonsPanel.add(cancelButton);
 clearButton = new JButton("Clear");
 clearButton.addActionListener(new ActionListener() {
   public void actionPerformed(ActionEvent e) {
     actionClear();
   }
 });
 clearButton.setEnabled(false);
 buttonsPanel.add(clearButton);

 // Add panels to display.
 getContentPane().setLayout(new BorderLayout());
 getContentPane().add(addPanel, BorderLayout.NORTH);
 getContentPane().add(downloadsPanel, BorderLayout.CENTER);
 getContentPane().add(buttonsPanel, BorderLayout.SOUTH);
}

// Exit this program.
private void actionExit() {
 System.exit(0);
}

// Add a new download.
private void actionAdd() {
 URL verifiedUrl = verifyUrl(addTextField.getText());
 if (verifiedUrl != null) {
   tableModel.addDownload(new Download(verifiedUrl));
   addTextField.setText(""); // reset add text field
 } else {
   JOptionPane.showMessageDialog(this,
     "Invalid Download URL", "Error",
     JOptionPane.ERROR_MESSAGE);
 }
}

// Verify download URL.
private URL verifyUrl(String url) {
 // Only allow HTTP URLs.
 if (!url.toLowerCase().startsWith("http://"))
   return null;

 // Verify format of URL.
 URL verifiedUrl = null;
 try {
   verifiedUrl = new URL(url);
 } catch (Exception e) {
   return null;
 }

 // Make sure URL specifies a file.
 if (verifiedUrl.getFile().length() < 2)
   return null;

 return verifiedUrl;
}

// Called when table row selection changes.
private void tableSelectionChanged() {
 /* Unregister from receiving notifications
    from the last selected download. */
 if (selectedDownload != null)
   selectedDownload.deleteObserver(DownloadManager.this);

 /* If not in the middle of clearing a download,
    set the selected download and register to
    receive notifications from it. */
 if (!clearing) {
   selectedDownload =
     tableModel.getDownload(table.getSelectedRow());
   selectedDownload.addObserver(DownloadManager.this);
   updateButtons();
 }
}

// Pause the selected download.
private void actionPause() {
 selectedDownload.pause();
 updateButtons();
}

// Resume the selected download.
private void actionResume() {
 selectedDownload.resume();
 updateButtons();
}

// Cancel the selected download.
private void actionCancel() {
 selectedDownload.cancel();
 updateButtons();
}

// Clear the selected download.
private void actionClear() {
 clearing = true;
 tableModel.clearDownload(table.getSelectedRow());
 clearing = false;
 selectedDownload = null;
 updateButtons();
}

/* Update each button's state based off of the
  currently selected download's status. */
private void updateButtons() {
 if (selectedDownload != null) {
   int status = selectedDownload.getStatus();
   switch (status) {
     case Download.DOWNLOADING:
       pauseButton.setEnabled(true);
       resumeButton.setEnabled(true);
       cancelButton.setEnabled(true);
       clearButton.setEnabled(false);
       break;
     case Download.PAUSED:
       pauseButton.setEnabled(false);
       resumeButton.setEnabled(true);
       cancelButton.setEnabled(true);
       clearButton.setEnabled(false);
       break;
     case Download.ERROR:
       pauseButton.setEnabled(false);
       resumeButton.setEnabled(true);
       cancelButton.setEnabled(false);
       clearButton.setEnabled(true);
       break;
     default: // COMPLETE or CANCELLED
       pauseButton.setEnabled(false);
       resumeButton.setEnabled(false);
       cancelButton.setEnabled(false);
       clearButton.setEnabled(true);
   }
 } else {
   // No download is selected in table.
   pauseButton.setEnabled(false);
   resumeButton.setEnabled(false);
   cancelButton.setEnabled(false);
   clearButton.setEnabled(false);
 }
}

/* Update is called when a Download notifies its
  observers of any changes. */
public void update(Observable o, Object arg) {
 // Update buttons if the selected download has changed.
 if (selectedDownload != null && selectedDownload.equals(o))
   updateButtons();
}

// Run the Download Manager.
public static void main(String[] args) {
 DownloadManager manager = new DownloadManager();
 manager.setVisible(true);
}
}
/*********************************************************************************************************************************
 * *******************************************************************************************************************************
 */
//This class manages the download table's data.
//The DownloadsTableModel class houses the Download Manager’s list of downloads and is
//the backing data source for the GUI’s “Downloads” JTable instance.

class DownloadsTableModel extends AbstractTableModel
implements Observer
{
/**
     *
     */
    private static final long serialVersionUID = 1L;

// These are the names for the table's columns.
private static final String[] columnNames = {"URL", "Size",
 "Progress", "Status"};

// These are the classes for each column's values.
@SuppressWarnings("rawtypes")
private static final Class[] columnClasses = {String.class,
 String.class, JProgressBar.class, String.class};

// The table's list of downloads.
private ArrayList<Download> downloadList = new ArrayList<Download>();


//The addDownload( ) method, shown here, adds a new Download object to the list of
//managed downloads and consequently a row to the table:
public void addDownload(Download download) {
 // Register to be notified when the download changes.
 download.addObserver(this);//This method first registers itself with the new Download as an Observer interested in receiving
                            //change notifications

 downloadList.add(download);// Next, the Download is added to the internal list of downloads being managed.


 // Fire table row insertion notification to table.
 fireTableRowsInserted(getRowCount() - 1, getRowCount() - 1);//Finally, a table row insertion event notification is fired to alert
                                                            //the table that a new row has been added

                                                             
}

// Get a download for the specified row.
public Download getDownload(int row) {
 return downloadList.get(row);
}

// Remove a download from the list.
public void clearDownload(int row) {
 downloadList.remove(row);

 // Fire table row deletion notification to table.
 fireTableRowsDeleted(row, row);//After removing the Download from the internal list, a table row deleted event notification is
                               // fired to alert the table that a row has been deleted.
}

// Get table's column count.
public int getColumnCount() {
 return columnNames.length;
}

// Get a column's name.
public String getColumnName(int col) {
  return columnNames[col];
}

// Get a column's class.
public Class<?> getColumnClass(int col) {//All columns are displayed as text (i.e., String objects) except for the Progress column, which
                                         //is displayed as a progress bar (which is an object of type JProgressBar).
 return columnClasses[col];     //
}

// Get table's row count.
public int getRowCount() {
 return downloadList.size();
}

// Get value for a specific row and column combination.
public Object getValueAt(int row, int col) {
 Download download = downloadList.get(row);
 switch (col) {
   case 0: // URL
     return download.getUrl();
   case 1: // Size
     int size = download.getSize();
     return (size == -1) ? "" : Integer.toString(size);
   case 2: // Progress
     return new Float(download.getProgress());
   case 3: // Status
     return Download.STATUSES[download.getStatus()];
 }
 return "";
}

/* Update is called when a Download notifies its
  observers of any changes */
public void update(Observable o, Object arg) {
 int index = downloadList.indexOf(o);

 // Fire table row update notification to table.
 fireTableRowsUpdated(index, index);
}
}
/*The DownloadsTableModel class essentially is a utility class utilized by the “Downloads”
JTable instance for managing data in the table. When the JTable instance is initialized, it is
passed a DownloadsTableModel instance. The JTable then proceeds to call several methods
on the DownloadsTableModel instance to populate itself. The getColumnCount( ) method
is called to retrieve the number of columns in the table. Similarly, getRowCount( ) is used to
retrieve the number of rows in the table. The getColumnName( ) method returns a column’s
name given its ID. The getDownload( ) method takes a row ID and returns the associated
Download object from the list.
*/

/*********************************************************************************************************************************
 * *******************************************************************************************************************************
 */
//This class renders a JProgressBar in a table cell.
class ProgressRenderer extends JProgressBar
implements TableCellRenderer
{
/**
     *
     */
    private static final long serialVersionUID = 1L;

// Constructor for ProgressRenderer.
public ProgressRenderer(int min, int max) {
 super(min, max);
}

/* Returns this JProgressBar as the renderer
  for the given table cell. */
public Component getTableCellRendererComponent(
 JTable table, Object value, boolean isSelected,
 boolean hasFocus, int row, int column)
{
 // Set JProgressBar's percent complete value.
 setValue((int) ((Float) value).floatValue());
 return this;
}
}
/*The ProgressRenderer class takes advantage of the fact that Swing’s JTable class
has a rendering system that can accept “plug-ins” for rendering table cells. To plug into
this rendering system, first, the ProgressRenderer class has to implement Swing’s
TableCellRenderer interface. Second, a ProgressRenderer instance has to be registered
with a JTable instance; doing so instructs the JTable instance as to which cells should be
rendered with the “plug-in.”
Implementing the TableCellRenderer interface requires the class to override the
getTableCellRendererComponent( ) method. The getTableCellRendererComponent( )
method is invoked each time a JTable instance goes to render a cell for which this class
has been registered. This method is passed several variables, but in this case only the value
variable is used. The value variable holds the data for the cell being rendered and is passed
to JProgressBar’s setValue( ) method. The getTableCellRendererComponent( ) method
wraps up by returning a reference to its class. This works because the ProgressRenderer
class is a subclass of JProgessbar, which is a descendent of the AWT Component class.
*/