Search code examples
javamultithreadingswingterminate

Cancel long task in javax.swing framework


I have a GUI with java.swing components, ActionListeners, and a SwingWorker to execute further code in a separate thread. I understand that a SwingWorker can only be created once and can't be terminated, but cancelled. Further I believe it to be good practice to check the SwingWorker status with its method isCancelled() and in case to exit the doInBackground() method and react in the done() method accordingly. This works fine if you have for example a loop within the doInBackground() method and can test isCancelled() at every iteration.

But how can you really break/terminate a long task that is executed within the doInBackground() method, such as reading a large csv (>1GB) or calling a process intensive method from another class? To illustrate my question I constructed a simple program that shows my problem when you choose a large input csv. The stop button works fine with the counter loop but doesn't terminate the csv import.

How can I actually break/terminate a long lasting process? If this isn't possible with SwingWorker, how would I do that with threads? Would thread.interrupt() be a possibility? How would I implement that in my example?

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.concurrent.TimeUnit;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import com.opencsv.CSVReader;

public class MinimalSwing extends JFrame {
// fields
private JButton fileButton, startButton, stopButton;
private JLabel displayLabel;
private File csvIn;
private SwingWorkerClass swingWorker;

// constructor
public MinimalSwing() {
    // set GUI-window properties
    setSize(300, 200);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setLocation(200, 200);
    setTitle("MinimalSwing");
    setLayout(new BorderLayout(9, 9));
    setResizable(false);

    // set components
    fileButton = new JButton("Choose File");
    fileButton.addActionListener(new ButtonActionListener());
    getContentPane().add("North", fileButton);
    startButton = new JButton("Start");
    startButton.setEnabled(false);
    startButton.addActionListener(new ButtonActionListener());
    getContentPane().add("West", startButton);
    stopButton = new JButton("Stop");
    stopButton.setEnabled(false);
    stopButton.addActionListener(new ButtonActionListener());
    getContentPane().add("East", stopButton);
    displayLabel = new JLabel("Status...");
    getContentPane().add("South", displayLabel);
}

// csvFileChooser for import
private File getCsv() {
    JFileChooser fc = new JFileChooser();
    int openDialogReturnVal = fc.showOpenDialog(null);
    if(openDialogReturnVal != JFileChooser.APPROVE_OPTION){
        System.out.println("ERROR: Invalid file choice.");
    }
    return fc.getSelectedFile();
}

// csvImporter
private class CsvImporter {
    public void readCsv(File file) throws IOException {
        CSVReader reader = new CSVReader(new FileReader(file));
        String [] nextLine;
        reader.readNext();  
        while ((nextLine = reader.readNext()) != null) {    
            displayLabel.setText("..still reading");
        }
        reader.close();
        displayLabel.setText("..actually done.");
    }
}

// ActionListener
private class ButtonActionListener implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent e) {
        if(e.getSource() == fileButton) {
            csvIn = getCsv();
            if(csvIn != null) {
                startButton.setEnabled(true);
                stopButton.setEnabled(true);
            }
        }
        else if(e.getSource() == startButton) {
            fileButton.setEnabled(false);
            startButton.setEnabled(false);
            stopButton.setEnabled(true);
            swingWorker = new SwingWorkerClass();
            swingWorker.execute();
        }
        else {
            fileButton.setEnabled(true);
            startButton.setEnabled(true);
            stopButton.setEnabled(false);
            swingWorker.cancel(true);
        }
    }
}

// swingWorker to interact with further program
private class SwingWorkerClass extends SwingWorker<Boolean, Void> {
    @Override
    protected Boolean doInBackground() throws Exception {
        long t0 = System.currentTimeMillis();
        displayLabel.setText("starting execution...");
        displayLabel.setText("..importing csv");
        CsvImporter csvImporter = new CsvImporter();
        csvImporter.readCsv(csvIn);
        if(isCancelled()) return false; // this cancels after the import, but I want to cancel during the import...
        long t1 = System.currentTimeMillis();
        displayLabel.setText("csv imported in " + String.format("%,d", t1 - t0) + " ms");
        for(short i=1; i<=10; i++) {
            if(isCancelled()) return false; // this works fine as it is called every second
            TimeUnit.SECONDS.sleep(1);
            displayLabel.setText("counter: " + i);
        }
        return true;
    }

    @Override
    public void done() {
        fileButton.setEnabled(true);
        startButton.setEnabled(true);
        stopButton.setEnabled(false);
        if(isCancelled()) {
            displayLabel.setText("Execution cancelled.");
        }
        else {
            displayLabel.setText("Execution succeeded.");
        }
    }
}

// main
public static void main(String[] args) throws URISyntaxException, IOException {
    // launch gui
    SwingUtilities.invokeLater(new Runnable() {
        public void run() {
            try {
                MinimalSwing frame = new MinimalSwing();
                frame.setVisible(true);
            }
            catch (Exception e) {
                e.printStackTrace();
            }
        }
    });
}

}


Solution

  • You can make your CSVImporter extend SwingWorker instead of having one more class SwingWorkerClass. In that way you can get more control and cancel the import task.

    Something like below.

      private class CsvImporter extends SwingWorker<Boolean, Void> {
    
            public boolean readCsv(File file) throws IOException {
                CSVReader reader = new CSVReader(new FileReader(file));
                String[] nextLine;
                reader.readNext();
                while ((nextLine = reader.readNext()) != null) {
                    displayLabel.setText("..still reading");
                    if (isCancelled())
                        return false; // this cancels after the import, but I want
                                        // to cancel during the import...
                }
                reader.close();
                displayLabel.setText("..actually done.");
                return true; // read complete
            }
    
            @Override
            protected Boolean doInBackground() throws Exception {
                long t0 = System.currentTimeMillis();
                displayLabel.setText("starting execution...");
                displayLabel.setText("..importing csv");
                CsvImporter csvImporter = new CsvImporter();
                boolean readStatus = csvImporter.readCsv(csvIn);
                if (readStatus) {
                    long t1 = System.currentTimeMillis();
                    displayLabel.setText("csv imported in " + String.format("%,d", t1 - t0) + " ms");
                    for (short i = 1; i <= 10; i++) {
                        if (isCancelled())
                            return false; // this works fine as it is called every second
                        TimeUnit.SECONDS.sleep(1);
                        displayLabel.setText("counter: " + i);
                    }
                }
                return readStatus;
            }
        }