JavaPush.javaComplete Source Code


/*
 * 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: *

    *
  1. DOWNLOADDIR indicates the location on the users HD for * which the files are to be downloaded eg. C:\downloaddir *
  2. 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 *
  3. 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 *
  4. 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); } }