views:

29

answers:

1

Hi,

when you download a resource in a Java Webstart application there's usually a download progress window displayed which shows the progress of the download. If this window is the default progress window, it has a cancel button. I'm basically trying to implement this cancel button in a custom download progress window.

As there is no method which you could call to cancel the download, I tried to find out how this was done in the default progress window. Because of the implementation with a ServiceManager it's a bit tricky to find the actual implementation. But I finally found this: [jdk-source on googlecode (DownloadServiceImpl)].

When you search for "cancel" or just scroll down to the progress method you will see that it should be as easy as throwing a RuntimeException. Sadly this doesn't really work. It just stops the progress method from being called. The resource is still downloaded in the background and the loadPart method never returns.

If you want to try this for yourself, I've prepared a small example. You will need some sort of webserver though (a local webserver is sufficient of course). I have tried this on a Windows XP (32 bit) with Java 1.6.0_21 (and apache tomcat 6).

A simple jnlp file would look like this (you probably want to change the port):

<?xml version="1.0" encoding="utf-8"?>
<jnlp 
  spec="1.0+"
  codebase="http://127.0.0.1:8080/DownloadTest" 
  href="DownloadTest.jnlp" 
  version="1.0">

  <information>
    <title>DownloadTest</title>
    <vendor>Download Tester</vendor>
  </information>

  <resources os="Windows">
    <java version="1.6.0_18+" href="http://java.sun.com/products/autodl/j2se" />
    <jar href="DownloadTest.jar" main="true"/>
    <jar href="largeResource.jar" download="lazy" part="One"/>
  </resources>

  <application-desc main-class="downloadtest.Main">
  </application-desc>
</jnlp>

Next you will need a large file as resource (the content doesn't matter at all). For example on many windows machines you you will find "driver.cab" under "Windows\Driver Cache\i386". The file must be added to a jar archive (jar -cf largeResource.jar <input file>).

The main program looks like this (you will need to include jnlp.jar as lib, which you can find at <jdk_home>\sample\jnlp\servlet):

package downloadtest;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import javax.jnlp.DownloadService;
import javax.jnlp.DownloadServiceListener;
import javax.jnlp.ServiceManager;
import javax.jnlp.UnavailableServiceException;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.SwingWorker;

public class Main {
    private static DownloadService downloadService;
    private static DownloadServiceListener customDownloadWindow;

    static {
        try {
            downloadService = (DownloadService) ServiceManager.lookup("javax.jnlp.DownloadService");
        } catch (UnavailableServiceException ex) {
            System.err.println("DownloadService not available.");
        }
        customDownloadWindow = new CustomProgress();
    }

    public static void main(String[] args) {
        JFrame frame = new JFrame("DownloadTest");
        frame.setBounds(0, 0, 200, 100);
        frame.setDefaultCloseOperation(JDialog.EXIT_ON_CLOSE);
        frame.setLayout(null);
        JButton startDownload = new JButton("download");
        startDownload.setBounds(20, 20, 150, 40);
        startDownload.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                new SwingWorker<Void, Void>() {
                    @Override
                    protected Void doInBackground() {
                        try {
                            downloadService.loadPart("One", customDownloadWindow);
                            //downloadService.loadPart("One", downloadService.getDefaultProgressWindow());
                        } catch (IOException ex) {
                            ex.printStackTrace();
                            System.err.println("IOException loadPart.");
                        }
                        return null;
                    }
                }.execute();
            }
        });
        frame.add(startDownload);
        frame.setVisible(true);
    }
}

You can try each download progress window by uncommenting one "downloadService.loadPart..." line and commenting out the other one.

And finally the custom progress window itself:

package downloadtest;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.net.URL;
import javax.jnlp.DownloadServiceListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.SwingUtilities;

public class CustomProgress implements DownloadServiceListener {
    JFrame frame = null;
    JProgressBar progressBar = null;
    boolean uiCreated = false;
    boolean canceled = false;

    public CustomProgress() {
    }

    private void create() {
        JPanel top = createComponents();
        frame = new JFrame(); // top level custom progress indicator UI
        frame.getContentPane().add(top, BorderLayout.CENTER);
        frame.setBounds(300,300,400,300);
        frame.pack();
        updateProgressUI(0);
    }

    private JPanel createComponents() {
        JPanel top = new JPanel();
        top.setBackground(Color.WHITE);
        top.setLayout(new BorderLayout(20, 20));

        String lblText = "<html><font color=green size=+2>JDK Documentation</font>" +
                   "<br/> The one-stop shop for Java enlightenment! <br/></html>";
        JLabel lbl = new JLabel(lblText);
        top.add(lbl, BorderLayout.NORTH);

        progressBar = new JProgressBar(0, 100);
        progressBar.setValue(0);
        progressBar.setStringPainted(true);
        top.add(progressBar, BorderLayout.CENTER);

        JButton cancelButton = new JButton("Cancel");
        cancelButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                CustomProgress.this.canceled = true;
            }
        });
        top.add(cancelButton, BorderLayout.SOUTH);

        return top;
    }

    public void progress(URL url, String version, long readSoFar,
                         long total, int overallPercent) {
        updateProgressUI(overallPercent);

    }

    public void upgradingArchive(java.net.URL url,
                      java.lang.String version,
                      int patchPercent,
                      int overallPercent) {
        updateProgressUI(overallPercent);
    }

    public void validating(java.net.URL url,
                java.lang.String version,
                long entry,
                long total,
                int overallPercent) {
        updateProgressUI(overallPercent);
    }


    public void downloadFailed(URL url, String string) {
        System.err.println("Download failed");
    }

    private void updateProgressUI(int overallPercent) {
        if (overallPercent > 0 && overallPercent < 99) {
            if (!uiCreated) {
                uiCreated = true;
                // create custom progress indicator's UI only if
                // there is more work to do, meaning overallPercent > 0 and < 100
                // this prevents flashing when RIA is loaded from cache
                create();
            }
            progressBar.setValue(overallPercent);
            if (canceled) {
                throw new RuntimeException("canceled by user");
            }
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    frame.setVisible(true);
                }
            });
        } else {
            // hide frame when overallPercent is above 99
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    if (frame != null) {
                        frame.setVisible(false);
                        frame.dispose();
                    }
                }
            });
        }
    }
}

This is basically taken from an Oracle tutorial (http://download.oracle.com/javase/tutorial/deployment/webstart/customProgressIndicatorForAppln.html). I just added a cancel button.

When you build this as a jar file and put it together with the largeResource.jar and DownloadTest.jnlp in a public folder of your webserver, you should be able to start the application via your web browser. Then click the download button and before it is finished click the cancel button in the download window. After trying the custom progress window you will need to remove the application (or just the resource) from your Java cache (because the resource is downloaded in the background regardless of clicking the cancel button).

So, why is this working with the default progress window but not with the custom progress window? Is there an easy possibility to cancel a download with a custom download window?

Any help or hints appreciated.

Drax

A: 

Ok, took a look at the Google sample you showed and found this at the bottom of the class

/* 
 * Progress Helper class
 *
 * The DownloadServiceListerner interface defined in the JNLP API is 
 * a subset of the DownloadProgressWindow interface used by elsewhere.
 *
 * this class is used to create a Helper object that implements both.
 */
private class ProgressHelper extends CustomProgress {


    private DownloadServiceListener _dsp = null;

    public ProgressHelper() {
        _dsp = null;
    }

    public ProgressHelper(DownloadServiceListener dsp) {
        setAppThreadGroup(Thread.currentThread().getThreadGroup());
        setListener(dsp);
        _dsp = dsp;
        if (_dsp instanceof DefaultProgressHelper) {
            ((DefaultProgressHelper) _dsp).initialize();
        }
        // for bug #4432604:
        _dsp.progress(null, null, 0, 0, -1);
    }

    public void done() {
        if (_dsp instanceof DefaultProgressHelper) {
            ((DefaultProgressHelper) _dsp).done();
        } else {
            // make sure callbacks to DownloadServiceListener have
            // been called before returning (for TCK test)
            flush();
        }
    }
}

And what is interesting is that it looks like it sets the current thread's ThreadGroup as the application thread group. So this leads me to believe that by doing this the actual download is kept closer to the application (not sure what the correct terminology would be) such that the RuntimeException throw in the class in the cancel check really does affect it. Otherwise, my hunch is that in your application the download actually takes place in another thread and is "unaffected" by the Exception thrown by your application, hence, allowing it to complete.

Chris Aldrich
Well I could set a DefaultUncaughtExceptionHandler for all threads (`Thread.setDefaultUncaughtExceptionHandler(new DownloadUncaughtExceptionHandler());`) which is probably similar to your solution (if not the same) and put the loadPart method into a Thread instead of a SwingWorker. This way the exception handler would be called, but the problem remains that the download of the resource continues in the background.
Drax
Is the download occurring in another thread? Can you get a handle on the thread?
Chris Aldrich