2

I am writing a simple Java Swing utility that will read messages from an MQ JMS server and display them in a JTable.

private void getMessages() {
        try {
            if (null != Queue) {
                Queue.close();  //Close previous queue connection if there is one.
            }
            Queue = new MQQueue(QueueManager, tableQueues.getValueAt(tableQueues.getSelectedRow(), 1).toString(), MQConstants.MQOO_INPUT_SHARED | MQConstants.MQOO_BROWSE | MQConstants.MQOO_OUTPUT, queueManager, null, null);
            int count = 0;
            modelMessages.setRowCount(0);
            MQGetMessageOptions getOptions = new MQGetMessageOptions();
            getOptions.options = MQConstants.MQGMO_BROWSE_FIRST;
            ArrayList<Object[]> rows = new ArrayList<Object[]>();
            while(true) {
                if (count > 0) {
                    getOptions.options = MQConstants.MQGMO_BROWSE_NEXT;
                }
                MQMessage message = new MQMessage();
                try {
                    Queue.get(message, getOptions);
                    byte[] b = new byte[message.getMessageLength()];
                    message.readFully(b);
                    rows.add(new Object[]{count + 1, new String(b)});
                    modelMessages.addRow(new Object[]{count + 1, new String(b)});
                    message.clearMessage();
                    count++;
                }
                catch (IOException e) {
                    break;
                }
                catch (MQException e) {
                    if (e.completionCode == 2 && e.reasonCode == MQConstants.MQRC_NO_MSG_AVAILABLE) {
                        break;
                    }
                }
            }
            modelMessages.fireTableDataChanged();
        } catch (MQException e) {
            txtMessage.setText("MQJE001: Completion Code '" + e.completionCode + "', Reason '" + e.reasonCode + "'.");
            modelMessages.setRowCount(0);
            modelMessages.fireTableDataChanged();
        }
    }

This works well for smaller queues but for large queues, it takes a while to populate this table and in the meantime, the Swing application is frozen so I am investigating ways to populate the JTable in the background while keeping not only the application but the JTable itself usable and scrollable during this.

I'm not very familiar with threading and I've tried a few things such wrapping some parts in SwingUtilities.invokeLater and implementing doInBackground() but nothing has seemed to work. Could someone please point me in the right direction of how to proceed with this?

EDIT

Based on the replies below, here is the solution:

public class GetMessagesWorker extends SwingWorker<DefaultTableModel, Object[]> {

    //http://stackoverflow.com/questions/22072480/java-updating-jtable-with-lots-of-rows-in-the-background#

    private final DefaultTableModel model;

    public GetMessagesWorker(DefaultTableModel model){
        this.model = model;
    }

    @Override
    protected DefaultTableModel doInBackground() throws Exception {
        try {
            if (null != Queue) {
                Queue.close();  //Close previous queue connection if there is one.
            }
            Queue = new MQQueue(QueueManager, tableQueues.getValueAt(tableQueues.getSelectedRow(), 1).toString(), MQConstants.MQOO_INPUT_SHARED | MQConstants.MQOO_BROWSE | MQConstants.MQOO_OUTPUT, queueManager, null, null);
            int count = 0;
            modelMessages.setRowCount(0);
            MQGetMessageOptions getOptions = new MQGetMessageOptions();
            getOptions.options = MQConstants.MQGMO_BROWSE_FIRST;
            while(true) {
                if (count > 0) {
                    getOptions.options = MQConstants.MQGMO_BROWSE_NEXT;
                }
                MQMessage message = new MQMessage();
                try {
                    Queue.get(message, getOptions);
                    byte[] b = new byte[message.getMessageLength()];
                    message.readFully(b);
                    Object[] row = {count + 1, new String(b)};
                    publish(row);
                    message.clearMessage();
                    count++;
                    if (isCancelled()) {
                        modelMessages.setRowCount(0);
                        count = 0;
                        message.clearMessage();
                        return model;
                    }
                }
                catch (IOException e) {
                    break;
                }
                catch (MQException e) {
                    if (e.completionCode == 2 && e.reasonCode == MQConstants.MQRC_NO_MSG_AVAILABLE) {
                        break;
                    }
                }
            }
            //modelMessages.fireTableDataChanged();
        } catch (MQException e) {
            txtMessage.setText("MQJE001: Completion Code '" + e.completionCode + "', Reason '" + e.reasonCode + "'.");
            modelMessages.setRowCount(0);
            modelMessages.fireTableDataChanged();
        }
        return model;
    }

    @Override
    protected void process(List<Object[]> chunks){
        for(Object[] row : chunks){
            model.addRow(row);
        }
    }

}

And here is the listener:

    tableQueues = new JTable(modelQueues);
    tableQueues.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
        public void valueChanged(ListSelectionEvent e) {
            if (!e.getValueIsAdjusting()) {
                txtMessage.setText("");
                if (tableQueues.getSelectedRow() > -1) {
                    gmw.cancel(false);
                    gmw = new GetMessagesWorker(modelMessages);
                    gmw.execute();
                }
            }
        }
    });
Matt
  • 2,503
  • 4
  • 31
  • 46

2 Answers2

5

When doing threading in Swing you need to understand that you have to do the work in the background thread but any UI updates have to be done in the Swing thread.

That means you should create a thread to fetch the messages. When you have one (or more), then use SwingUtilities to synchronize with the Swing thread and update the table.

In the code snippet above, that means you need to use SwingUtilities every time that you invoke a method of modelMessages, txtMessage, etc.

Since that's pretty expensive, you will usually collect, say, 10 new rows in list and then add them all at once using a single call to SwingUtilities.invokeLater().

Aaron Digulla
  • 321,842
  • 108
  • 597
  • 820
5

This is due to Swing architecture. You must understand that the same thread where your listener is being executed is responsible for far more things (for example, refresh your UI).

I would use in your case a SwingWorker. This is a kind of Swing-specific background thread with special features to safely send updates to your UI as it executes its long task. You would do there your long task in background and call publish() for every relevant result it retrieves. Every time publish() is called, your process() method would be called (sending an update to the main thread under the hood), there you could update your table model.

Here you can find more information on SwingWorker:

http://docs.oracle.com/javase/tutorial/uiswing/concurrency/worker.html

Jorge_B
  • 9,712
  • 2
  • 17
  • 22
  • +1 for the SwingWorker and link to the Swing tutorial. – camickr Feb 27 '14 at 15:55
  • SwingWorker seems to be working for me and I have it nearly working (will update question with solution once complete). One problem - my application has two tables - left is a list of JMS queues and right is a list of messages in the highlighted queue. When you select a new queue, the messages will be shown in the right table. I have "new GetMessagesWorker(modelMessages).execute();" in the ActionListener and this works but if I change queues while it is still populating, it does not cleanly end and goes on for a little bit longer after the focus change. How can I resolve this? – Matt Feb 27 '14 at 16:07
  • Maybe you can keep record of your open `SwingWorkers` and define some synchronized status property on it. Check it inside your main loop before polling your JMS queue, and update it from the outside to 'stop' when you would like it to stop... I don't know if it is your best option but I think it would work – Jorge_B Feb 27 '14 at 16:33
  • Something like that seems to work. It's not super clean but it does the trick - thank you. – Matt Feb 27 '14 at 16:44
  • there are three important issue with SwingWorker and MQ, SwingWorker isn't designated to be reusable, for periodiacal getXxx() from MQ is better to use util.Timer and invokeLater for updates Swing GUI or JComponents XxxModel, exception (by default whole architecture) MQ is pretty asynchronous, [by default you can to lost an exception by using SwingWorker](http://stackoverflow.com/questions/7053865/cant-get-arrayindexoutofboundsexception-from-future-and-swingworker-if-threa), – mKorbel Feb 28 '14 at 08:35
  • addeum (my personal view) I think that SwingWorker isn't good choice for production code (black holes in Future and SwingWorkers APIs), even this forum is SwingWorker possitive – mKorbel Feb 28 '14 at 08:36
  • @Matt your notifiers for XxxTableModel are wrong, nor, never ever to be called outside of class that overrode XxxTableModels methods (a few times asked here with code where is possible to simulating a real impact to JTable, its model) – mKorbel Feb 28 '14 at 08:39
  • @Matt your usage of SwingWorker is totally wrong, you have to test get(possible exception is possible to read only in done()), [you have to override done(there are two, three really important reasons](http://stackoverflow.com/questions/7053865/cant-get-arrayindexoutofboundsexception-from-future-and-swingworker-if-threa), use util.Timer or Runnable#Thread for any asynchronous data sources – mKorbel Feb 28 '14 at 08:43