tags:

views:

69

answers:

2

I'm working on swing application that relies on an embedded H2 database. Because I don't want to bundle the database with the app(the db is frequently updated and I want new users of the app to start with a recent copy), I've implemented a solution which downloads a compressed copy of the db the first time the application is started and extracts it. Since the extraction process might be slow I've added a ProgressMonitorInputStream to show to progress of the extraction process - unfortunately when the extraction starts, the progress dialog shows up but it's not updated at all. It seems like to events are getting through to the event dispatch thread. Here is the method:

public static String extractDbFromArchive(String pathToArchive) {
    if (SwingUtilities.isEventDispatchThread()) {
        System.out.println("Invoking on event dispatch thread");
    }

    // Get the current path, where the database will be extracted
    String currentPath = System.getProperty("user.home") + File.separator + ".spellbook" + File.separator;
    LOGGER.info("Current path: " + currentPath);

    try {
        //Open the archive
        FileInputStream archiveFileStream = new FileInputStream(pathToArchive);
        // Read two bytes from the stream before it used by CBZip2InputStream

        for (int i = 0; i < 2; i++) {
            archiveFileStream.read();
        }

        // Open the gzip file and open the output file
        CBZip2InputStream bz2 = new CBZip2InputStream(new ProgressMonitorInputStream(
                              null,
                              "Decompressing " + pathToArchive,
                              archiveFileStream));
        FileOutputStream out = new FileOutputStream(ARCHIVED_DB_NAME);

        LOGGER.info("Decompressing the tar file...");
        // Transfer bytes from the compressed file to the output file
        byte[] buffer = new byte[1024];
        int len;
        while ((len = bz2.read(buffer)) > 0) {
            out.write(buffer, 0, len);
        }

        // Close the file and stream
        bz2.close();
        out.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException ex) {
        ex.printStackTrace();
    }

    try {
        TarInputStream tarInputStream = null;
        TarEntry tarEntry;
        tarInputStream = new TarInputStream(new ProgressMonitorInputStream(
                              null,
                              "Extracting " + ARCHIVED_DB_NAME,
                              new FileInputStream(ARCHIVED_DB_NAME)));

        tarEntry = tarInputStream.getNextEntry();

        byte[] buf1 = new byte[1024];

        LOGGER.info("Extracting tar file");

        while (tarEntry != null) {
            //For each entry to be extracted
            String entryName = currentPath + tarEntry.getName();
            entryName = entryName.replace('/', File.separatorChar);
            entryName = entryName.replace('\\', File.separatorChar);

            LOGGER.info("Extracting entry: " + entryName);
            FileOutputStream fileOutputStream;
            File newFile = new File(entryName);
            if (tarEntry.isDirectory()) {
                if (!newFile.mkdirs()) {
                    break;
                }
                tarEntry = tarInputStream.getNextEntry();
                continue;
            }

            fileOutputStream = new FileOutputStream(entryName);
            int n;
            while ((n = tarInputStream.read(buf1, 0, 1024)) > -1) {
                fileOutputStream.write(buf1, 0, n);
            }

            fileOutputStream.close();
            tarEntry = tarInputStream.getNextEntry();

        }
        tarInputStream.close();
    } catch (Exception e) {
    }

    currentPath += "db" + File.separator + DB_FILE_NAME;

    if (!currentPath.isEmpty()) {
        LOGGER.info("DB placed in : " + currentPath);
    }

    return currentPath;
}

This method gets invoked on the event dispatch thread (SwingUtilities.isEventDispatchThread() returns true) so the UI components should be updated. I haven't implemented this as an SwingWorker since I need to wait for the extraction anyways before I can proceed with the initialization of the program. This method get invoked before the main JFrame of the application is visible. I don't won't a solution based on SwingWorker + property changed listeners - I think that the ProgressMonitorInputStream is exactly what I need, but I guess I'm not doing something right. I'm using Sun JDK 1.6.18. Any help would be greatly appreciated.

+3  A: 

While you're running your extraction process on the EDT, it will block all updates to the GUI, even the progress monitor. This is exactly the situation that SwingWorker will help in.

In effect, you're blocking painting by hogging the EDT to do your database extraction. Any GUI update requests (such as calls to repaint()) will be queued up, but actually repainting those updates never gets to run, because the EDT is already busy.

The only way it can possibly work is if you offload the processing to another thread. SwingWorker makes it easier to do just that.

Ash
I considered what you're saying here, but I think that the Swing engineer probably had in mind that input streams are generally read in loops. If the loop is really hogging the EDT completely that would mean that this ProgressMonitorInputStream is always useless...
Bozhidar Batsov
I understand what you're saying. I can't really see the use case either, because if you use it on a thread other than the EDT, it's going to call `setValue` (amongst other things) on the underlying `JProgressBar`, which is itself breaking Swing's rules. But it doesn't change the reality that if you block the EDT, your GUI will not update. You shouldn't do I/O on the EDT. Also, every example I've googled on PMIS is using either the main thread or a swing worker to do the actual reading from the stream.
Ash
My problem is that I have to wait for the SwingWorker thread to complete before I can resume with my init sequence. If I call join on the EDT the situation will be as bad as now. Most of the time when I've used monitor I have some button that the user presses - this starts a worker thread that disables a couple of UI elements so the user is force to wait for the thread to finish. But since this extraction happens at a point that no UI elements are visible I cannot play like this and I don't won't to introduce unnecessary dialogs which might alleviate the situation.
Bozhidar Batsov
+2  A: 

@Ash is right about SwingWorker. You can use done() to enable your other GUI features when ready. The done() method will be "executed on the Event Dispatch Thread after the doInBackground() method is finished."

trashgod
I'm aware of that. I don't do Swing development from yesterday :-) As I already wrote in my previous comment I need to wait for the swing worker to finish before resuming the EDT since right after the piece of code I showed in the question I create an entity manager using the downloaded db. And there are no gui features that I can disable so that I can enable them with done. The way I see it - if there is no way to sync in the current setup - I'd have to rework my init logic just for a progress bar...
Bozhidar Batsov
As the user must wait for initialization to complete, display progress in a separate, "splash" window. When completed, replace that window with your GUI.
trashgod
I do this from the very beginning. I just wanted some stuff to visible outside the splash since they aren't actually part of the apps initialization, but a actually a prelude to it. No matter - I worked around the problem by refactoring my pre-init code a bit. Thank for the help. +1 from me.
Bozhidar Batsov
Excellent! Also, consider Java Web Start as an alternative mechanism for periodic updates.
trashgod
@trashgod: Nice followup. @Bozhidar: Glad to hear you got it going
Ash