JavaPush.javaComplete Source Code
- By Shawn Bedard
- June 13, 2000
/*
* Copyright (c) 1999,2000 Bowne Internet Solutions,
* All Rights Reserved.
*/
import java.applet.AppletContext;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URLConnection;
import java.net.URL;
import javax.swing.JLabel;
import javax.swing.JApplet;
import javax.swing.JPanel;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import javax.swing.SwingConstants;
import java.awt.GridLayout;
import java.awt.Font;
import java.awt.Color;
import java.util.Vector;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.StringTokenizer;
import java.util.NoSuchElementException;
import java.io.File;
import java.io.OutputStream;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.IOException;
import java.io.BufferedOutputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.BufferedInputStream;
import java.io.OutputStream;
/**
* This applet is used to compare the version of files on the
* client and server. This comparison is done by comparing manifest
* files that are located on both the client and server. There is no
* comparison on actual file dates and sizes, so the manifest files must
* be kept up to date with the files.
*
*
There are 4 param tags that are needed for this applet:
*
* - DOWNLOADDIR indicates the location on the users HD for
* which the files are to be downloaded eg. C:\downloaddir
*
- NEXTPAGEURL indicates the target URL of the next page to hit once
* all the files have been downloaded. eg /Login.html http://www.mysite.com/Intro.html
*
- SERVERDIR points to where the files that need to be downloaded exist. This is relative
* from the server where the applet was downloaded from eg. /download
*
- MANIFEST points to the relative location of the manifest file. On the client this
* location is relative to DOWNLOADDIR. On the server this location is relative to SERVERDIR.
*
* Since this applet needs to be signed this example uses Sun's signing
* tool along with the JavaPluging to support Java 1.2. The plugin requires
* different param tags from regular applets. An example is shown below.
*
*<code>
*<OBJECT classid="clsid:8AD9C840-044E-11D1-B3E9-00805F499D93"
* WIDTH = 400 HEIGHT = 75 codebase="http://java.sun.com/products/plugin/1.2.2/jinstall-122-win32.cab#Version=1,2,2,0">
*<PARAM NAME = CODE VALUE = "JavaPush.class" >
*<PARAM NAME = CODEBASE VALUE = "/javapush">
*<PARAM NAME = ARCHIVE VALUE = "/javapush/JavaPush.jar">
*<PARAM NAME="type" VALUE="application/x-java-applet;version=1.2">
*<PARAM NAME = DOWNLOADDIR VALUE = "C:/javapush">
*<PARAM NAME = NEXTPAGEURL VALUE = "/javapush/Login.html">
*<PARAM NAME = SERVERDIR VALUE = "/download">
*<PARAM NAME = MANIFEST VALUE = "/push/VersionManifest.txt">
*<COMMENT>
*<EMBED
* type="application/x-java-applet;version=1.2"
* java_CODE = JavaPush.class
* java_CODEBASE = "/javapush"
* java_ARCHIVE = "/javapush/JavaPush.jar"
* WIDTH = 400
* HEIGHT = 75
* DOWNLOADDIR = "C:/javapush"
* NEXTPAGEURL = "/javapush/Login.html"
* SERVERDIR = "/download"
* MANIFEST = "/push/VersionManifest.txt"
* pluginspage="http://java.sun.com/products/plugin/1.1.2/plugin-install.html">
* <NOEMBED></COMMENT>
*
*</NOEMBED></EMBED>
*</OBJECT>
*</code>
*
*
* Manifest files contain information about the file path and
* the version information associated with a particular file.
* Information for a particular file resides on a single line and is
* delimited with a # character. A sample manifest file would contain
* text as follows:
* <code>
* /Instructions.txt#1.0.01
* /sourceFiles/JavaPushBackup.jar#1.0.00
* </code>
*
*
If there is a discrepancy between the manifest files then all files
* differing will be downloaded to the client in the specified directory
* listed in the manifest file.
*
* @author Shawn Bedard
* @version $Revision: 11 $, $Date: 12/02/99 4:38p $
*/
public class JavaPush extends JApplet
implements Runnable
{
/**************************
* set default values
***************************/
private static String PCDownloadDir = "/downloaddir"; //where the application downloads to
private static String NextPageUrl = "Login.html"; //Url for the next page to be targeted
//after the push applet
private static String ServerPushRetrievalDir = "/pushtest"; //Location of the files to be
//downloaded
private static String ManifestFile = "/push/VersionManifest.txt"; //Location of the Manifest
//file (both client and server)
private static final String FILE_SEPARATOR = "/"; /* Delimeter that separates directories in the Manifest -- Normally one would use
* System.getProperty("file.separator"), however since the user can time anything
* they want in the Manifest this must be hard coded. Note: Java will convert / to \
* when creating directories in the PC environment. */
private String ServerManifestFile = ServerPushRetrievalDir + ManifestFile; //where to retrieve
//the server manifest from
private String ClientManifestFile = PCDownloadDir + ManifestFile; //where to retrieve the
//client manifest from
private static final String MANIFEST_DELIM = "#"; //the delimiter used in the manifest file
private static final String URL_DIRECTORY_DELIM = "/"; //directory delimiter for URLS (this
//should not be changed)
// Constants for the applet param tags
private final String PC_DOWNLOADDIR_PARAM = "DOWNLOADDIR";
private final String NEXT_PAGE_PARAM = "NEXTPAGEURL";
private final String SERVER_PUSH_RETRIEVAL_DIR_PARAM = "SERVERDIR";
private final String MANIFEST_FILE_PARAM = "MANIFEST";
// necessary system properties
// private final static String LINE_SEP = System.getProperty("line.separator");
//one would guess this would work but it renders as a box in a JOptionPane;
private final static String LINE_SEP = "\n";
private final static int HTTP_OK = 200;
private final String MANIFEST_MIMETYPE = "text/plain";
private final static String INFOTEXT = "Retrieving Update from Server";
// GUI components
private JPanel mainPanel;
private JLabel infoLabel;
private JLabel statusLabel;
// URL for the server containing the application files. For now
// this only supports download servers from which the applet came.
private URL serverUrl;
/*
* This init determines the current environment and retrieves the necessary
* applet parameters. It also creates and lays out any of the GUI components.
* None of the downloading is done in the init, instead the process to
* accomplish this is done by putting a process on the Swing thread. This
* allows the applet to be repainted before any downloading is done.
*/
public void init()
{
// get the current location of the applet.
AppletContext context = getAppletContext();
URL oldURL = getDocumentBase();
try
{
serverUrl = new URL("http", oldURL.getHost(), "" );
}
catch (MalformedURLException e)
{
System.err.println( "Malformed URL exception: " + e.toString() );
}
//get the data needed from the param tags.
getAppletParameters();
// create and layout all the necessary GUI components.
createComponents();
layoutComponents();
// run the downloading stuff later so the applet repaints.
SwingUtilities.invokeLater(this);
}
/**
* The run methods sets off the logic to check the manifest files and
* start the downloads if necessary. If the server manifest can not be
* obtained then the program can not execute. In this case a print statement
* is sent to System.err and the program exits.
*/
public void run()
{
// After the download is complete the login page must be presented.
// This creates the target page URL of the login.
URL nextPageURL = null;
try
{
nextPageURL = new URL(getDocumentBase(), NextPageUrl);
}
catch (MalformedURLException e)
{
System.err.println("Could not create URL: " + NextPageUrl );
return;
}
final URL finalNextPageUrl = nextPageURL;
//determine the files to be downloaded and get them from the server
try
{
Vector downloadList = retrieveDownLoadList();
createDirectoriesFor(downloadList);
if (downloadList.size() > 0)
{
downloadFiles(downloadList);
}
// Queue the redirect action on the Swing thread after the download events.
// Since each download is being queued on the swing thread from the preceding
// calls we don't want to retarget another page before the files are finished
// downloading.
SwingUtilities.invokeLater
(
new Runnable()
{
public void run()
{
getAppletContext().showDocument(finalNextPageUrl);
}
}
);
}
catch (Exception e)
{
System.err.println("Java Push Error:" + e.toString() );
return;
}
}
/**
* Tries to obtain the manifest lists and compares them. If the Server
* manifest list is not found then an IOException is thrown. If the
* Client manifest list is not found then all the files from the server
* list are returned. If both retrievals are successful then the lists are
* compare and only the out of date files from the client are downloaded
*
* @return Enumeration of strings containing the list of filed to be downloaded.
* @exception IOException thrown if server manifest could not be retrieved.
*/
public Vector retrieveDownLoadList()
throws IOException
{
Hashtable serverManifestHash = null;
Hashtable clientManifestHash = null;
serverManifestHash = getServerManifest();
clientManifestHash = getClientManifest();
return compareManafestFiles(serverManifestHash, clientManifestHash);
}
/**
* Utility method to create the directories needed for the download. This
* method recurses through the files and creates the directories that are
* needed. If the directory is already created then no action is taking.
*
* @param Vector list of files that will need directories to be created
*/
public void createDirectoriesFor(Vector list)
throws Exception
{
Enumeration files = list.elements();
// always check for the root directory -- it must be there
createDirectory( PCDownloadDir + FILE_SEPARATOR );
while (files.hasMoreElements())
{
String file = (String)files.nextElement();
createDirectory(PCDownloadDir + file);
}
}
/**
* Utility method to create a directory. If the directory already exists
* then no action will be taken. If unable to create the directory then an
* IOException is thrown.
*
* @param string containing the directory to be created.
*/
public void createDirectory(String dir)
throws Exception
{
File directory =
new File(dir.substring(0, dir.lastIndexOf(FILE_SEPARATOR)) );
// create the directory only if it doesn't exist.
if (!directory.exists())
{
if ( !directory.mkdirs() )
{
throw new Exception("Could not create directory " + directory.toString() );
}
}
}
/**
* Method to download the list of files passed to it. The files will
* be retrieved from the server in the push directory. This method also
* sends an information message to the user indicating which files have
* been downloaded.
*
* @param Enumeration containing the list of files
*/
private void downloadFiles(Vector vectorListOFiles)
{
String stringListOfFiles = vectorToString(vectorListOFiles);
Enumeration listOFiles = vectorListOFiles.elements();
new JOptionPane().showMessageDialog(null,
"The following files are out of date:" +
LINE_SEP + stringListOfFiles + LINE_SEP +
" Click OK to Download",
"Message",
JOptionPane.INFORMATION_MESSAGE);
while (listOFiles.hasMoreElements())
{
final String filename = (String)listOFiles.nextElement();
// The following is a little weird so I will explain. As you can see below
// two events are put in the swing thread for each file download. The reason
// for this is to ensure that the GUI gets updated each time a files gets
// downloaded. The first invokeLater creates a runnable to update the GUI and
// the second invokeLater actually does the download. The problem with repainting
// on the same thread as the download is that all the repaint events get delayed
// until ALL the files are downloaded. Hence, the user isn't notified in time. This
// delay occurs even if paintImmediately is called. It seems that paintImmediately
// really means paint when you feel like it and doesn't paint when I want it to. So,
// by placing paint and download events on the Swing thread queue I can get the applet
// to repaint when I want. If any of you find a better way of doing this please let
// me know. ;)
SwingUtilities.invokeLater
(
new Runnable()
{
public void run()
{
String message = "Downloading " + filename;
statusLabel.setText(message);
statusLabel.paintImmediately(0,0,350,40);
getAppletContext().showStatus(message);
}
}
);
// download the file
SwingUtilities.invokeLater
(
new Runnable()
{
public void run()
{
try
{
HttpURLConnection huc = setupHTTPRequest(serverUrl,
ServerPushRetrievalDir + filename);
downloadFile(huc, filename);
}
catch (IOException e)
{
System.err.println("Could not download file:" + filename +
" because of the following error:" + e.toString() );
}
}
}
);
}
}
/***************************************************************
* Manifest Utility Methods
***************************************************************/
/**
* Retrieve the Client Manifest from Disk. If this file does not exist
* then return an empty hash. Returning the empty hash will cause the entire
* application to be downloaded
*
* @return Hash containing the list of files with their versions listed in the client manifest
*/
private Hashtable getClientManifest()
{
File clientManifest = null;
Hashtable clientManifestHash = null;
clientManifest = new File(ClientManifestFile);
try
{
FileInputStream fis = new FileInputStream(clientManifest);
InputStreamReader isr = new InputStreamReader(fis);
BufferedReader br = new BufferedReader(isr);
clientManifestHash = parseManifest(br);
// this will close the FileInputStream and InputStreamReader as well
br.close();
}
catch(java.security.AccessControlException fpe)
{
System.err.println("Client Manifest File " + ClientManifestFile
+" could not be accessed. Downloading entire application." );
return new Hashtable();
}
catch (FileNotFoundException fnfe)
{
System.err.println("Client Manifest File " + ClientManifestFile
+" does not exist. Downloading entire application." );
return new Hashtable();
}
catch (IOException ioe)
{
System.err.println("Client Manifest File " + ClientManifestFile
+" could not be read in. Downloading entire application." );
return new Hashtable();
}
return clientManifestHash;
}
/**
* Retrieve the Manifest file from the server and put the information received
* in the tempServerHash Hash.
*
* @return Hash containing the list of files with their versions listed in the server manifest
* @exception IOException is thrown if the server Manifest can not be received.
*/
public Hashtable getServerManifest()
throws IOException
{
Hashtable tempServerHash;
HttpURLConnection huc = setupHTTPRequest(serverUrl, ServerManifestFile);
// receive the reply
BufferedInputStream bis = new BufferedInputStream(huc.getInputStream());
InputStreamReader isr = new InputStreamReader( bis );
BufferedReader br = new BufferedReader( isr );
// MIME type check -- make sure we are receiving a text file.
String mimeType = huc.getContentType();
if ( mimeType.equals(MANIFEST_MIMETYPE))
{
System.out.println("Reply MIME type is okay");
}
else
{
throw new WrongMimeTypeException("Manifest file is wrong MIME type: " + mimeType);
}
tempServerHash = parseManifest(br);
// this closes BufferedInputStream and InputStreamReader as well
br.close();
return tempServerHash;
}
/**
* This method compares the manifest files retrieved from the server and the
* client. Files that exist on the server and not client will be downloaded
* in addition to the files that differ in version.
*
* @param serverManifestHash hashtable containing the list of files
* with their versions listed in the server manifest
* @param clientManifestHash hashtable containing the list of files
* with their versions listed in the client manifest
*/
public Vector compareManafestFiles(Hashtable serverManifestHash, Hashtable clientManifestHash)
{
Vector downList = new Vector();
Enumeration serverKeys = serverManifestHash.keys();
System.out.println("Comparing files");
while ( serverKeys.hasMoreElements() )
{
String serverKey = (String)serverKeys.nextElement();
if ( clientManifestHash.containsKey(serverKey) && fileExists(serverKey) )
{
// add to the download list if the values differ
if ( !((String)clientManifestHash.get(serverKey)).trim().equals(
((String)serverManifestHash.get(serverKey)).trim() ) )
{
downList.addElement(serverKey);
}
}
else
{ //if the key doesn't exist on the client OR the file is missing
//add it to the download list
downList.addElement(serverKey);
}
}
// always download the manifest file if there are files
// to be downloaded.
if (downList.size() > 0)
{
downList.addElement(ManifestFile);
}
return downList;
}
/**
* A utility method that determines if a specific file listed in the ServerManifest Exists
* @return boolean indicating if the file exists
*/
private boolean fileExists(String file)
{
File testFile = new File( PCDownloadDir + file );
return testFile.exists();
}
/***************************************************************
* Communication Methods
***************************************************************/
/**
* This method is used to actually download the file from a given HttpURLConnection.
* Both the input and output streams are set up and the file is downloaded from
* the URL connection
* @param huc is the HttpURLConnection from where the file will be retrieved from
* @param downloadFile is the name of the file to be downloaded.
* @exception IOException is thrown if the file can not be retrieved.
*/
public static void downloadFile(HttpURLConnection huc, String downloadFile)
throws IOException
{
// initialize local vars
int dataread = 0;
int count = 0;
int CHUNK_SIZE = 8192; // TCP/IP packet size
byte[] dataChunk = new byte[CHUNK_SIZE]; // byte array for storing temporary data.
// Check for valid MIME type (only retrieve jars, text and gifs)
String mimeType = huc.getContentType();
if ( huc.getResponseCode() != HTTP_OK )
{
throw new WrongStatusCodeException(downloadFile, huc.getResponseCode());
}
BufferedOutputStream bos = null;
BufferedInputStream bis = null;
try
{
// create file and stream to write the file (throws IOException)
File destinationFile = new File( PCDownloadDir + downloadFile );
FileOutputStream fos = new FileOutputStream(destinationFile);
bos = new BufferedOutputStream(fos);
// receive the file using a BufferedReader
bis = new BufferedInputStream(huc.getInputStream());
InputStreamReader isr = new InputStreamReader( bis );
BufferedReader br = new BufferedReader( isr );
// do the actual download and let the user know of the status
while (dataread >= 0)
{
count++;
dataread = bis.read(dataChunk,0,CHUNK_SIZE);
// only write out if there is data to be read
if ( dataread > 0 )
bos.write(dataChunk,0,dataread);
}
// make sure you finish your write to the file
bos.flush();
}
finally
{
// don't forget to close the streams (even if exception is thrown)
if (bis != null) { bis.close(); }
if (bos != null) { bos.close(); }
}
}
/**
* Form an HttpURLConnection to retrieve a specific file from the server. This
* method only creates the connection. The downloading is not done in this step.
*
* @param serverUrlDestination an URL containing the server information from which
* the file is to be retrieved
* @param destinationPath Sting containing the path and file information needed.
*/
public static HttpURLConnection setupHTTPRequest(
URL serverUrlDestination, String destinationPath)
throws IOException
{
URL downloadingURL;
HttpURLConnection huc = null;
// create the URL
downloadingURL = new URL("http", serverUrlDestination.getHost(), destinationPath );
// create the HTTP URL Connection
try
{
URLConnection uc = downloadingURL.openConnection();
huc = (HttpURLConnection)uc;
}
catch (IOException ioe)
{
throw new IOException("Failed opening connection to server at URL " +
downloadingURL.toString() + ": " + ioe.toString());
}
// setup headers and stuff
huc.setRequestMethod("GET");
huc.setAllowUserInteraction(false);
huc.setDoOutput(true);
huc.setDoInput(true);
huc.setDefaultUseCaches(false);
huc.setUseCaches(false);
return huc;
}
/***************************************************************
* Utility methods
***************************************************************/
/**
* Utility method to take a manifest file from a buffered reader and
* place it into a HashTable
* @param br the BufferedReader pointing to the manifest file
* @return a hashtable containing all the information from the manifest table.
* @exception IOException is thrown if manifest file contains no valid entries
*/
private Hashtable parseManifest(BufferedReader br)
throws IOException
{
String line;
Hashtable tempHashtable = new Hashtable();
while ( ( line = br.readLine() ) != null )
{
try
{
StringTokenizer st = new StringTokenizer(line,MANIFEST_DELIM);
tempHashtable.put(st.nextToken(),st.nextToken());
}
catch ( NoSuchElementException nsee )
{
System.err.println("Ignoring non-parsable line: " + line);
}
}
if ( tempHashtable.isEmpty() )
{
throw new IOException( "Data stream empty" );
}
return tempHashtable;
}
/**
* Utility method to convert a vector into a String
* @param v Vector to be converted into a String
* @return String of the converted Vector
*/
public String vectorToString(Vector v)
{
StringBuffer sb = new StringBuffer();
Enumeration e = v.elements();
while ( e.hasMoreElements() )
sb.append(e.nextElement().toString()).append(LINE_SEP);
return sb.toString();
}
/***************************************************************
* GUI methods
***************************************************************/
/**
* Get the necessary data from the applet tags.
*/
private void getAppletParameters()
{
// get the download directory from the param tag. Right now
// this does not do any OS checking, however, it would be nice
// to add it sometime in the future.
String directory = getParameter(PC_DOWNLOADDIR_PARAM);
if ( directory != null )
{
System.out.println("Setting download directory to " + directory);
PCDownloadDir = directory;
ClientManifestFile = PCDownloadDir + ManifestFile;
} else
{
System.out.println("Param not found.. leaving download directory as " + PCDownloadDir );
}
// get the URL from the param tag if possible.
String tempURL = getParameter(NEXT_PAGE_PARAM);
if ( tempURL != null )
{
System.out.println("Setting next page target URL to " + tempURL);
NextPageUrl = tempURL;
} else
{
System.out.println("Param not found.. leaving next page URL as " + NextPageUrl);
}
// get the location of the manifest file (this is relative to the ServerPushRetrievalDir!)
String tempManifestFile = getParameter(MANIFEST_FILE_PARAM);
if ( tempManifestFile != null )
{
System.out.println("Setting manifest file to " + tempManifestFile);
ManifestFile = tempManifestFile;
ClientManifestFile = PCDownloadDir + ManifestFile;
} else
{
System.out.println("Param not found.. leaving as " + ManifestFile);
}
// get the location of the files to be downloaded.
String tempServerDir = getParameter(SERVER_PUSH_RETRIEVAL_DIR_PARAM);
if ( tempServerDir != null )
{
ServerPushRetrievalDir = tempServerDir;
if ( ServerPushRetrievalDir.endsWith(URL_DIRECTORY_DELIM) )
{
ServerPushRetrievalDir = ServerPushRetrievalDir.substring
(0, ServerPushRetrievalDir.length() - 1);
}
System.out.println("Setting server download directory to " + ServerPushRetrievalDir);
ServerManifestFile = ServerPushRetrievalDir + ManifestFile;
} else
{
System.out.println("Param not found.. leaving server download dir as "
+ ServerPushRetrievalDir);
}
}
/**
* Creates all the necessary components on the applet
*/
private void createComponents()
{
// The label and the panel containing it
infoLabel = new JLabel(INFOTEXT, JLabel.CENTER);
infoLabel.setVerticalAlignment( SwingConstants.CENTER );
infoLabel.setFont( new Font("Helvetica", Font.PLAIN, 18) );
// status label for showing status of downloads
statusLabel = new JLabel("Validating current version of application", JLabel.CENTER);
statusLabel.setVerticalAlignment( SwingConstants.CENTER );
statusLabel.setFont( new Font("Helvetica", Font.PLAIN, 14) );
}
/**
* Layout the created components on the applet
*/
private void layoutComponents()
{
mainPanel = new JPanel();
mainPanel.setLayout(new GridLayout(2,1));
mainPanel.add( infoLabel );
mainPanel.add( statusLabel );
getContentPane().add(mainPanel);
}
}