4

I've created a simple Java application that each second for for 10 seconds consecutive seconds adds a new row to a JTable. It consists of three classes.

The main class that gets called once the program is started

public class JarBundlerProblem {
    public static void main(String[] args)
    {
        System.err.println("Initializing controller");
        new Controller();
    }
}

A controller that creates the GUI and alters it through doWork()

public class Controller {
    public Controller()
    {
        doWork(null);
    }
    public static void doWork(String s)
    {
        GUI gui = new GUI();
        
        for (int i=0; i<10; i++)
        {
            gui.addRow("Line "+(i+1));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

And finally, the GUI

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.DefaultTableModel;

public class GUI {
    private JFrame frame = new JFrame();
    private DefaultTableModel model = new DefaultTableModel();
    private JTable table = new JTable(model);
    private JScrollPane pane = new JScrollPane(table);
    
    public GUI()
    {
        model.addColumn("Name");
        
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(pane);
        frame.pack();
        frame.setVisible(true);
    }
    public void addRow(String name)
    {
        model.addRow(new Object[]{name});
    }
}

Since I'm developing for OS X, and I need to be able to associate my application with a certain file type (let's say .jarbundlerproblem), I have to bundle my JAR file into an APP using Apple Jar Bundler. I have done this successfully, my application opens, counts to ten, writing out each second.

Now, for the problem

By default, double-clicking a .jarbundlerproblem, and associating the file with my application, will not pass the file I double-clicked as an argument to the application. Apparently, this is just Java on OS X works.

Since I need to be able to see what file was double-clicked, I'm using OSXAdapter which is a Java library made by Apple for the purpose. This, I've implemented by altering the constructor of my Controller class and added another method registerForMacOSXEvents():

public Controller()
{
    registerForMacOSXEvents();
    //doWork(null);
}
public void registerForMacOSXEvents() {
    try {
        OSXAdapter.setFileHandler(this, getClass().getDeclaredMethod("doWork", new Class[] { String.class }));
    } catch (Exception e) {
        System.err.println("Error while loading the OSXAdapter:");
        e.printStackTrace();
    }
}

But after this (minor) modification, my application starts acting up. Sometimes, it doesn't open, even though I can see in the Console that it just started (Initializing controller is written), but after a few attempts, it will eventually start, but the windows will be completely blank for the first 10 seconds, and after that, the 10 rows will be added.

Help

Now, I've struggled with this quite a bit, and it seems like there isn't a lot of documentation regarding neither OSXAdapter nor Jar Bundler. What am I doing wrong? Or shouldn't I be using OSXAdapter or Jar Bundler in the first place?

Community
  • 1
  • 1
kba
  • 19,333
  • 5
  • 62
  • 89
  • just to make sure I understand the OSXAdapter (not familiar with mac): basically it's a thread running - outside of your control - which reports its results by messaging the doWork with a string (filename?) until its ready, starting immediately after registration? How does it signal being ready? – kleopatra Sep 27 '11 at 09:30
  • glad to help :-) Would you please show your solution (without the pending list)? – kleopatra Sep 27 '11 at 13:22
  • @kleopatra I don't know exactly how `OSXAdapter` works, I've just only familiarized myself with it. If you're interested in knowing more about it, its source is available at http://developer.apple.com/library/mac/#samplecode/OSXAdapter/Listings/src_OSXAdapter_java.html#//apple_ref/doc/uid/DTS10000685-src_OSXAdapter_java-DontLinkElementID_5 – kba Sep 27 '11 at 14:53
  • @kleopatra My solution is done by taking your idea and implementing it into my application which is a lot more code than what is presented above, so showing my solution will be difficult. I can explain what I've done though: All the work that you had in `run()` I've moved to `doInBackground()`, and instead added `execute()`to `run()`. `doWork()` has been removed completely. Whenever an iteration in `Controller.doInBackground()` is done, it just calls `publish()`. – kba Sep 27 '11 at 14:56
  • hmm ... dont quite get it, but if it's working :-) Just beware: the doc states that a SwingWorker is designed to execute once - there might be side-effects if you invoke that repeatedly – kleopatra Sep 27 '11 at 15:09
  • The point is that you're doing all the work in `run()`, then calling `doWork()` and have that call `doInBackground()`. There is no need for three methods; one will suffice. Additionally, if the `Thread.sleep()` is removed from `doInBackground()`, the `while` loop might stop and `doInBackground()` terminates, so the last rows won't get published to the GUI. This can no longer occur either. – kba Sep 27 '11 at 15:15

3 Answers3

6

It looks like you're blocking the event dispatch thread(EDT). SwingWorker would be a better choice, but this example implements Runnable.

Addendum: You might look at this project for an example of MVC architecture. It also shows how to construct a Mac OS application bundle without using JAR Bundler. More on MVC may be found here.

As an aside, this example shows one approach to auto-scrolling a JTable. Click on the thumb to suspend scrolling; release to resume.

Addendum: Your application lags for 10 seconds on startup. As this is the exact time for which the Controller sleeps, it's surely sleeping on the EDT. An sscce would be dispositive. Instead, do the work on another thread and update the model on the EDT. SwingWorker has a process() method that does so automatically, or you can use invokeLater() as shown below. Until your application is correctly synchronized, there's little hope of getting Apple events to work.

Addendum: You can invoke isDispatchThread() in the Controller to check. The project cited includes a .dmg with a Mac application and an ant file that builds the bundle in situ via target dist2.

Addendum: See also the alternate approaches shown here.

enter image description here

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.DefaultTableModel;

/** @seehttps://stackoverflow.com/questions/7519244 */
public class TableAddTest extends JPanel implements Runnable {

    private static final int N_ROWS = 8;
    private static String[] header = {"ID", "String", "Number", "Boolean"};
    private DefaultTableModel dtm = new DefaultTableModel(null, header) {

        @Override
        public Class<?> getColumnClass(int col) {
            return getValueAt(0, col).getClass();
        }
    };
    private JTable table = new JTable(dtm);
    private JScrollPane scrollPane = new JScrollPane(table);
    private JScrollBar vScroll = scrollPane.getVerticalScrollBar();
    private JProgressBar jpb = new JProgressBar();
    private int row;
    private boolean isAutoScroll;

    public TableAddTest() {
        this.setLayout(new BorderLayout());
        jpb.setIndeterminate(true);
        this.add(jpb, BorderLayout.NORTH);
        Dimension d = new Dimension(320, N_ROWS * table.getRowHeight());
        table.setPreferredScrollableViewportSize(d);
        for (int i = 0; i < N_ROWS; i++) {
            addRow();
        }
        scrollPane.setVerticalScrollBarPolicy(
            JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
        vScroll.addAdjustmentListener(new AdjustmentListener() {

            @Override
            public void adjustmentValueChanged(AdjustmentEvent e) {
                isAutoScroll = !e.getValueIsAdjusting();
            }
        });
        this.add(scrollPane, BorderLayout.CENTER);
        JPanel panel = new JPanel();
        panel.add(new JButton(new AbstractAction("Add Row") {

            @Override
            public void actionPerformed(ActionEvent e) {
                addRow();
            }
        }));
        this.add(panel, BorderLayout.SOUTH);
    }

    private void addRow() {
        char c = (char) ('A' + row++ % 26);
        dtm.addRow(new Object[]{
                Character.valueOf(c),
                String.valueOf(c) + String.valueOf(row),
                Integer.valueOf(row),
                Boolean.valueOf(row % 2 == 0)
            });
    }

    private void scrollToLast() {
        if (isAutoScroll) {
            int last = table.getModel().getRowCount() - 1;
            Rectangle r = table.getCellRect(last, 0, true);
            table.scrollRectToVisible(r);
        }
    }

    @Override
    public void run() {
        while (true) {
            EventQueue.invokeLater(new Runnable() {

                @Override
                public void run() {
                    addRow();
                }
            });
            EventQueue.invokeLater(new Runnable() {

                @Override
                public void run() {
                    scrollToLast();
                }
            });
            try {
                Thread.sleep(1000); // simulate latency
            } catch (InterruptedException ex) {
                System.err.println(ex);
            }
        }
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                JFrame f = new JFrame();
                f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                TableAddTest nlt = new TableAddTest();
                f.add(nlt);
                f.pack();
                f.setLocationRelativeTo(null);
                f.setVisible(true);
                new Thread(nlt).start();
            }
        });
    }
}
Community
  • 1
  • 1
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • I suspect the bundling simply exposes the latent synchronization problem. – trashgod Sep 22 '11 at 18:14
  • Could you explain how to do this if `addRow()` isn't called from within the GUI class? – kba Sep 22 '11 at 18:35
  • @Kristian: Move the update method into your `TableModel` subclass, and pass a reference to the model via the external `Runnable`'s constructor. – trashgod Sep 22 '11 at 19:15
  • Sorry, I don't follow. My update method? Do you mean `GUI.addRow()`? And do you want me to override the `addRow()` method of `model`? You lost me. – kba Sep 22 '11 at 21:09
  • You can override the model's `addRow()`, but I'd prefer composition. Keep the model and view separate, as discussed in this [answer](http://stackoverflow.com/questions/3066590/gui-problem-after-rewriting-to-mvc/3072979#3072979). – trashgod Sep 22 '11 at 21:38
  • @trashgod: How would overriding the `DefaultTableModel.addRow()` help at all? The problem is that method doesn't get called when it should. And yes, I would also prefer to keep it seperated, I love the MVC pattern, but I still don't get what you suggest I do with my application. – kba Sep 26 '11 at 21:13
  • No, you say I'm doing work on EDT, which I'm not. All work is done in Controller. Additionally, you come with a suggestion of making an OS X Application without JAR Bundler, but the project you link to is just JAR files; an unbundled application, which means it doesn't involve OSXAdapter that I depend on. – kba Sep 26 '11 at 23:11
  • I believe you are mistaken on both counts; more above. – trashgod Sep 27 '11 at 02:45
  • 1
    @KristianAntonsen what you are doing wrong is the _access_ of the ui _off_ the EDT. All change _must_ happen on the EDT, no exception to the rule. Trashgod shows how to do it with invokeLater, another (in your context better option, IMO) is implementing a SwingWorker - read its api doc to see an example which fits perfectly to your problem. – kleopatra Sep 27 '11 at 09:07
  • See also the alternate approaches shown [here](http://stackoverflow.com/a/10461026/230513). – trashgod May 05 '12 at 11:01
4

After doing it, I'm not fully convinced a SwingWorker is a simpler (aka: better) solution - still requires additional thread synching (between the worker thread and the "outer" thread which passes in the file/names). Anyway (taking the opportunity to learn, and be it by errors :), below is a crude proof of concept example for the basic idea:

  • implement the Controller as SwingWorker, which funnels the input from the outer thread into the EDT
  • make it accept input (from the adapter, f.i.) via a method doWork(..) which queues the input for publishing
  • implement doInBackground to succesively publish the input

open issues

  • synch the access to the local list (not an expert in concurrency, but pretty sure that needs to be done)
  • reliably detecting the end of the outer thread (here simply stops when the input queue is empty)

Feedback welcome :-)

public class GUI {
    private JFrame frame = new JFrame();
    private DefaultTableModel model = new DefaultTableModel();
    private JTable table = new JTable(model);
    private JScrollPane pane = new JScrollPane(table);

    public GUI() {
        model.addColumn("Name");

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(pane);
        frame.pack();
        frame.setVisible(true);
    }

    public void addRow(String name) {
        model.addRow(new Object[] { name });
    }

    /**
     * Controller is a SwingWorker.
     */
    public static class Controller extends SwingWorker<Void, String> {
        private GUI gui;

        private List<String> pending;

        public Controller() {
            gui = new GUI();
        }

        public void doWork(String newLine) {
            if (pending == null) {
                pending = new ArrayList<String>();
                pending.add(newLine);
                execute();
            } else {
                pending.add(newLine);
            }
        }

        @Override
        protected Void doInBackground() throws Exception {
            while (pending.size() > 0) {
                publish(pending.remove(0));
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return null;
        }

        /**
         * @inherited <p>
         */
        @Override
        protected void process(List<String> chunks) {
            for (String object : chunks) {
                gui.addRow(object);
            }
        }

    }

    /** 
     * Simulating the adapter.
     * 
     *  Obviously, the real-thingy wouldn't have a reference 
     *  to the controller, but message the doWork refectively 
     */
    public static class Adapter implements Runnable {

        Controller controller;

        public Adapter(Controller controller) {
            this.controller = controller;
        }

        @Override
        public void run() {
            for (int i=0; i<10; i++)
            {
                controller.doWork("Line "+(i+1));
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }
    public static void main(String[] args)
    {
        System.err.println("Initializing controller");
        new Adapter(new Controller()).run();
    }

    @SuppressWarnings("unused")
    private static final Logger LOG = Logger.getLogger(GUI.class.getName());
}
kleopatra
  • 51,061
  • 28
  • 99
  • 211
  • +1 Re open issues: using `synchronized` on `doWork()` should be sufficient, and the `get()` method of `SwingWorker` will wait until its background thread exits. I've added continuous [variation](http://stackoverflow.com/questions/7519244/jar-bundler-using-osxadapter-causing-application-to-lag-or-terminate/7568947#7568947) nearby. – trashgod Sep 27 '11 at 12:20
  • Thanks, kleopatra, using your example I finally managed to get it all working. I did, however separate the classes and move all the work into `doInBackground` and got rid of the `pending` list all together. – kba Sep 27 '11 at 13:13
  • I agree that `SwingWorker` is the better choice; it sounds like there's no need for a pending queue. – trashgod Sep 27 '11 at 16:51
2

Here's a variation of @kleopatra's example in which a continuously running Controller accepts new entries in doWork(), while a SwingWorker processes the pending entries asynchronously in its background thread. ArrayBlockingQueue handles the synchronization.

import java.awt.EventQueue;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingWorker;
import javax.swing.table.DefaultTableModel;

public class GUI {

    private static final Random rnd = new Random();
    private JFrame frame = new JFrame();
    private DefaultTableModel model = new DefaultTableModel();
    private JTable table = new JTable(model);
    private JScrollPane pane = new JScrollPane(table);

    public GUI() {
        model.addColumn("Name");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(pane);
        frame.pack();
        frame.setVisible(true);
    }

    public void addRow(String name) {
        model.addRow(new Object[]{name});
    }

    /**
     * Controller is a SwingWorker.
     */
    private static class Controller extends SwingWorker<Void, String> {

        private static final int MAX = 5;
        private GUI gui;
        private BlockingQueue<String> pending =
            new ArrayBlockingQueue<String>(MAX);

        public Controller() {
            EventQueue.invokeLater(new Runnable() {

                @Override
                public void run() {
                    gui = new GUI();
                }
            });
        }

        private void doWork(String newLine) {
            try {
                pending.put(newLine);
            } catch (InterruptedException e) {
                e.printStackTrace(System.err);
            }
        }

        @Override
        protected Void doInBackground() throws Exception {
            while (true) {
                // may block if nothing pending
                publish(pending.take());
                try {
                    Thread.sleep(rnd.nextInt(500)); // simulate latency
                } catch (InterruptedException e) {
                    e.printStackTrace(System.err);
                }
            }
        }

        @Override
        protected void process(List<String> chunks) {
            for (String object : chunks) {
                gui.addRow(object);
            }
        }
    }

    /** 
     * Exercise the Controller.
     */
    private static class Adapter implements Runnable {

        private Controller controller;

        private Adapter(Controller controller) {
            this.controller = controller;
        }

        @Override
        public void run() {
            controller.execute();
            int i = 0;
            while (true) {
                // may block if Controller busy
                controller.doWork("Line " + (++i));
                try {
                    Thread.sleep(rnd.nextInt(500)); // simulate latency
                } catch (InterruptedException e) {
                    e.printStackTrace(System.err);
                }
            }
        }
    }

    public static void main(String[] args) {
        System.out.println("Initializing controller");
        // Could run on inital thread via
        // new Adapter(new Controller()).run();
        // but we'll start a new one
        new Thread(new Adapter(new Controller())).start();
    }
}
Community
  • 1
  • 1
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • Thanks for the answer, but I completely removed the `pending` from my actual project. :-) – kba Sep 27 '11 at 13:16
  • thanks - ArrayBlockingQueue is my learn item of the day :-) and +1 for creating the ui on the EDT, my forgetfulness is legendary ... – kleopatra Sep 27 '11 at 13:26
  • @kleopatra: I always welcome your insights. I'm glad you found `ArrayBlockingQueue` interesting. I've updated the code to use the interface, [`BlockingQueue`](http://download.oracle.com/javase/7/docs/api/java/util/concurrent/BlockingQueue.html), which has implementations for a variety of use cases. – trashgod Sep 27 '11 at 16:48