GordonM
GordonM

Reputation: 31780

Interfacing Swing with a thread other than a worker

I've built a simple client and server for a project, using a very basic text interface for testing and development (code demonstrating usage below).

The client library has an acceptMessage() method and a getMessage() method, which basically push messages into or pull messages out of a blockingQueue for input and output respectively (in the client this is implemented with put() and take() calls). One thread blocks on System.in and sends read lines to the client via acceptMessage whilst the other blocks on the client getMessage() method and echoes messages to System.out when they arrive. All very basic but it works okay.

Now that I've gotten my client library to work I'm trying to figure out how to integrate it into an application that uses a Swing GUI. So far I've not got much further than building a simple form in the interface building tool in Netbeans, with a text input box and a label. The idea is for the text input box to take the place of reading from system.in and the label to display what would have otherwise been written to system.out. By that point I'll have replicated my simple test app in Swing.

From what I understand, everything that interacts with a Swing GUI directly has to run in the Swing thread, but the client is built to run as its own thread. I don't think sending a message to acceptMessage() from the GUI will be terribly difficult (I think it involves setting up an ActionPerformed method that will read the contents of the input box and call acceptMessage() on the client, though I'm still trying to figure it out), but I have no idea how to go about getting responses back. I know I can't have the client invoke functionality in the GUI thread because of thread safety issues, and besides the client library is written in such a way as to not know anything about its consuming class. A client instance is simply passed into the consuming class which then uses acceptMessage() and getMessage() to send and receive messages. It does not (and should not) care what the consuming class actually is.

Is it possible with this architecture to easily integrate the client into the GUI? If so, what's the correct way of handling input from the client? (Like I said, I think output to the client isn't going to be especially difficult once I've figured out that aspect of Swing).

My primary motivation for using Swing is that a) it seems to be the best documented GUI library for Java, b) Netbeans already has tools for working with Swing and c) I have a rigid deadline and don't have time to switch GUI libraries and start from scratch (this is for a university project). Had I more time I'd have probably looked into other libraries, but I imagine they'd all have their own sets of quirks as well.

import java.io.*;
import java.util.logging.*;

public class TestApp implements Runnable {

    private Client <String>     client              = null;
    private UpstreamChannel     upstream            = null;
    private DownstreamChannel   downstream          = null;
    private Thread              upstreamThread      = null;
    private Thread              downstreamThread    = null;
    private boolean             ending              = false;

    private class UpstreamChannel implements Runnable {

        private TestApp outer   = null;

        @Override
        public void run () {

            Thread.currentThread ().setName ("TestApp.UpstreamChannel");

            try (BufferedReader inReader = new BufferedReader (new InputStreamReader (System.in))) {
                while (!this.outer.ending) {
                    this.outer.client.acceptMessage (inReader.readLine ());
                }
            } catch (IOException ex) {
                Logger.getLogger (this.getClass ().getName ()).log (Level.SEVERE, ex.getMessage (), ex);
            } finally {
                this.outer.ending   = true;
                this.outer.downstreamThread.interrupt ();
                Thread.currentThread ().interrupt ();
                return;
            }
        }

        public UpstreamChannel (TestApp app) {
            this.outer  = app;
        }
    }

    private class DownstreamChannel implements Runnable {

        private TestApp outer   = null;

        @Override
        public void run () {

            Thread.currentThread ().setName ("TestApp.DownstreamChannel");

            try {
                while (!this.outer.ending) {
                    System.out.println (this.outer.client.getMessage ());
                }
            } catch (InterruptedException ex) {
                Logger.getLogger (this.getClass ().getName ()).log (Level.INFO, ex.getMessage (), ex);
            } finally {
                this.outer.ending   = true;
                this.outer.upstreamThread.interrupt ();
                Thread.currentThread ().interrupt ();
                return;
            }
        }

        public DownstreamChannel (TestApp app) {
            this.outer  = app;
        }
    }

    @Override
    public void run () {
        if ((null == this.upstreamThread) 
        && (null == this.downstreamThread)) {
            this.upstreamThread     = new Thread (this.upstream);
            this.downstreamThread   = new Thread (this.downstream);
            this.upstreamThread.start ();
            this.downstreamThread.start ();

            try {
                this.upstreamThread.join ();
                this.downstreamThread.join ();
            } catch (InterruptedException ex) {
                Logger.getLogger (this.getClass ().getName ()).log (Level.INFO, ex.getMessage (), ex);
            } finally {
                this.upstreamThread.interrupt ();
                this.downstreamThread.interrupt ();
                System.out.println ("Sayonara");
            }
        }
    }

    public TestApp (Client <String> client) {
        this.upstream   = new UpstreamChannel (this);
        this.downstream = new DownstreamChannel (this);
        this.client     = client;

        Logger.getLogger (this.getClass ().getName ()).log (Level.INFO, "Class instantiated");
    }
}

The code for starting the client app is as follows:

    public static void main (String[] args) throws UnknownHostException, IOException, InterruptedException {
        Client <String> clientInstance  = new Client ("localhost", 8113);
        TestApp         app             = new TestApp (clientInstance);
        Thread          clientThread    = new Thread (clientInstance);
        Thread          appThread       = new Thread (app);

        clientThread.start ();
        appThread.start ();

        clientThread.join ();
        appThread.interrupt ();

        System.exit (0);
    }
}

EDIT: I thought a worker thread that polls getMessage (and therefore blocks until a message arrives) and uses publish() to make it available would be a solution, but I think there's a risk of messages getting "dropped on the floor" if two messages arrive in quick succession.

    SwingWorker reader                      = new SwingWorker () {
        @Override
        protected Object doInBackground () throws Exception {
            while (!this.isCancelled ()) {
                publish (clientInstance.getMessage ());
            }
            return null;
        }
    };

Upvotes: 0

Views: 98

Answers (2)

chubbsondubs
chubbsondubs

Reputation: 38885

I personally like to follow something like this in your actionPerformed method:

public void actionPerformed( ActionEvent event ) {
    Promise<SomeReturn> promise = server.acceptMessage( message, SomeReturn.class );
    promise.onSuccess( new Callback() {
        public void call( SomeReturn result ) {
            // this is on the swing thread
            someLabel.setText( result.getText() );
        }
    });
}

Then this is the promise class for asynchronous operations:

public class Promise<T> {

    Callback<T> successCallback;
    Callback<? extends Exception> errorCallback;

    // swing client will register callbacks with these methods
    public onSuccess( Callback<T> callback ) {
        successCallback = callback;
    }

    // swing client will register callbacks with these methods
    public Promise<T> onError( Callback<? extends Exception> callback ) {
        errorCallback = callback;
    }

    // network client thread will call these methods when it gets a response
    public void setResult( final T result ) {
        SwingUtilities.invokeLater( new Runnable() {
            public void run() {
               if( successCallback != null ) 
                   successCallback.call( result );
               else 
                   log.warning("Response dropped!  No success callback registered " + result );
            }
        });
    }

    // network client thread will call these methods when it gets a response
    public void setError( final Exception ex ) {
        SwingUtilities.invokeLater( new Runnable() {
            public void run() {
               if( errorCallback != null ) 
                   errorCallback.call( ex );
               else 
                   log.error( ex );
            }
        });
    }
}

I don't like using something like getMessage() because that requires a synchronous style coding where the swing client has to poll or know when to call getMessage() so that it won't block the thread. Instead why not let the thread performing the work call you back when a response arrives. It does that by calling setResult() or setError() on the Promise it returned. You request another thread to perform an action by calling acceptMessage(), then it returns to you a Promise. You can register with that promise to be notified when that message is processed (either success or error).

In Java SE API we have Future which is really close to this idea of Promise, but unfortunately they didn't provide a callback mechanisms to be notified when the result arrives. It's tremendously disappointing the API authors didn't do that because the interface they created is pretty much useless.

Behind the acceptMessage() implementation could be lots of things. You could spin up a thread passing the message and promise to it to service that message, or you could drop the message + promise onto a queue that a consumer thread or thread pool draws from. When it finishes processing the message it uses the promise to call back to your UI.

Upvotes: 1

nos
nos

Reputation: 229334

So your basic problem is how to get

while (!this.outer.ending) {
      System.out.println (this.outer.client.getMessage ());
 }

to set the text on a label instead of printing to stdout. You're right that since this is a separate thread, you can't call a method directly on a GUI widget. But you can ask the GUI thread to dispatch some code that will get run in the GUI thread,

while (!this.outer.ending) {
  final String message = this.outer.client.getMessage (); 

    java.awt.EventQueue.invokeLater(new Runnable() {

      public void run() {
           theLabel.setText(message);      

       }
    });
}

The other way around might be as easy as having an event handler in your GUI call

  this.outer.client.acceptMessage (textBox.getText());

However if acceptMessage is also a blocking call, you need a separate thread here as well, and have the GUI communicate the message over to that thread.

Upvotes: 2

Related Questions