The Source for Java Technology Collaboration
User: Password:



Evan Summers

Evan Summers's Blog

Swing and Roundabouts 1M: Emission DTs

Posted by evanx on August 31, 2006 at 09:12 AM | Comments (0)

(*)

Preface

This article might be debunked over time at this permalink on java.net CVS. Actually corrections have probably already been made to that copy, during a post-publication review (or two), so maybe click through to there rather than continue here.

Cos it's so much easier to right-click and commit a subdirectory from Netbeans, than to edit weblogs using blogowarez. Not to mention uploading accompanying files one-by-one... So there's another great use for Netbeans/CVS - to edit and publish blog entries :)

Introduction
"I can't believe it! Reading and writing actually paid off!" Homer Simpson

Swing and Roundabouts 1P: Epoxy DTs looked at using dynamic proxies to avoid boiler-plate code to build GUI helper classes whose methods should all run in the EDT, even if they are invoked from a background thread. For example, a long running task in a SwingWorker thread might require confirmation dialogs, GUI focus changes and such, which should be invoked in the EDT.

Another option is eventbus.dev.java.net by Michael Bushe, which uses the subscribe/publish model, eg. one can publish events to invoke subscribers inside the EDT. This is more loosely coupled than Swing listeners, since those need to be attached to specific components.

Since this seems to be an interesting approach that I haven't used before, let's implement a simplistic subscribe/publish mechanism, and use it for some dialogs and what-not, to see what it's all about.

Wherez the warez?

If you want the warez before the hows, here is the demo...

(*) (QuiteBusy, Java5, 450k, unsandboxed)

Which looks something like this...

(*)

Which looks, and is, very ordinary, except that the dialogs, console and status bar, are called into action using a message bus, for loose-coupling and what-not.

The code is currently in the quitebusy and quitebusydemo packages on vellum.dev.java.net, but will move to aptfoundation.dev.java.net at some stage, and thereafter maybe to quitebusy.dev.java.net one day. You can check out the vellum project, and run the same main class as in the JNLP, ie. quitebusydemo.common.QMessageBusDemo.

Message Bus Interfaces

Our main QMessageBus interface below exposes methods to subscribe and unsubscribe services for specific message types, and of course also methods to publish messages.

public interface QMessageBus<Response> {
    public void subscribe(Class messageClass, QSubscriber subscriber);
    public void subscribeWeakly(Class messageClass, QSubscriber subscriber);
    public void subscribeResponder(Class messageClass, QResponder subscriber) throws Throwable;
    public void unsubscribe(Class messageClass, QSubscriber subscriber);
    public void publish(QMessage message) throws Throwable;
    public void publishBackground(QMessage message);
    public Response getResponse(QMessage message) throws Throwable;
    public void publishException(Throwable exception, String text);
    public void publishInfo(Level level, String text);
}

In a minute we'll present an implementation where publishBackground() uses a background SwingWorker thread outside the EDT, and publish() uses invokeAndWait() on the EDT.

We parameterise the message bus implementation with a Response type, rather than let getResponse() return an Object, out of habit more than anything else. It makes most sense to default the Response to Object, so as not to limit our options.

Our QMessage interface is just a tag as follows.

public interface QMessage {
}

Our QSubscriber interface is as follows.

public interface QSubscriber<Message> {
    public void receive(Message message) throws Exception;
}

where a subscriber receives a message, and might throw an exception.

When subscribing a service (to receive messages published via the bus), we can opt to subscribe "weakly" using subscribeWeakly(), in which case we will use Java's "weak references." These don't prevent garbage collection (when there are only weak references to an object). This is handy for subscribers that might come and go.

Our QResponder interface below is similar to QSubscriber above, except that our "responder" can return an object.

public interface QResponder<Message, Response> {
    public Response receive(Message message) throws Exception;
}

We subscribe a "responder" service for a given message type using subscribeResponder(). In this case, we can use the getResponse() method to wait for the response, and return the response object. For example, we might publish a message for a "confirmation dialog" service to display, where it returns a Boolean object. This is essentially a loosely-coupled (and thinly-veiled) method invocation. So it makes sense to have only a single responder for a given message type.

Incidently, in a future article, we might try to push our luck with a distributed message bus, perhaps using RESTful messages, eg. QBasicRestMessage with QRestMessageType of POST, GET, PUT and DELETE, and the URI and what-not. Those might be serialised into XML, and sent over HTTP to remote QRestResource responders. Sounds like too much fun not to try!

Message Bus Implementation Singleton

We implement a QDefaultMessageBus as follows.

In order to support a "logging aspect," we use a proxy object for the messageBus singleton. This delegates the method invocation to our NLoggingInvoker, for logging methods, their arguments, and return value (if there is one). Actually this is just an exercise to reuse our dynamic proxy classes introduced in Epoxy DTs :)

Rather than getResponse() return an generic object, we use generics to limit our options to a Boolean response type, at least for our messageBus singleton below.

We have two subscriber maps, one for weak references to subscribers, and another for strong references. And we have a responder map too.

public class QDefaultMessageBus<Response> implements QMessageBus<Response> {
    public static final QMessageBus<Boolean> messageBusProxy = (QMessageBus)
        NProxyFactory.createProxy(new QDefaultMessageBus(), new NLoggingInvoker());
    
    public static final QDefaultMessageBus<Boolean> messageBus = new QDefaultMessageBus();
    
    QMessageBusConfiguration configuration = new QMessageBusConfiguration();
    QPublisher<Response> publisher = new QEdtPublisher();
    QSubscriberMap weakSubscriberMap = new QSubscriberMap(QWeakSubscriberList.class);
    QSubscriberMap defaultSubscriberMap = new QSubscriberMap(QDefaultSubscriberList.class);
    Map<Class, QResponder> responderMap = Collections.synchronizedMap(new HashMap());
    
    protected QDefaultMessageBus() {
    }
    
    public synchronized void subscribe(Class messageClass, QSubscriber subscriber) {
        QSimpleList subscriberList = defaultSubscriberMap.getSubscriberList(messageClass);
        if (subscriberList.contains(subscriber)) {
            logger.warning(messageClass, subscriber);
        } else {
            subscriberList.add(subscriber);
        }
    }
    
    public synchronized void subscribeWeakly(Class messageClass, QSubscriber subscriber) {
        QSimpleList subscriberList = weakSubscriberMap.getSubscriberList(messageClass);
        if (subscriberList.contains(subscriber)) {
            logger.warning(messageClass, subscriber);
        } else {
            subscriberList.add(subscriber);
        }
    }
    
    public synchronized void subscribeResponder(Class messageClass, QResponder responder) 
        throws Throwable {
        if (responderMap.get(messageClass) != null) {
            throw new NRuntimeException(
                configuration.subscribeResponderExclusiveError, messageClass);
        } else {
            responderMap.put(messageClass, responder);
        }
    }
    
    public synchronized void unsubscribe(Class messageClass, QSubscriber subscriber) {
        defaultSubscriberMap.getSubscriberList(messageClass).remove(subscriber);
        weakSubscriberMap.getSubscriberList(messageClass).remove(subscriber);
        responderMap.remove(messageClass);
    }
    ...
}

We now implement methods to publish messages, eg. via invokeAndWait() in the EDT, or alternatively in the background, eg. using a SwingWorker thread, ie. outside of the EDT. If our task will take any length of time to execute, then we must use publishBackground() to avoid blocking the EDT, otherwise the GUI will appear to be "hung" while the long task is executing.

Services that receive messages via publishBackground() must themselves use publish() to invoke any GUI services eg. popping up dialogs and such, to ensure that those run in the EDT. Otherwise they could use EDT-safe helper classes, as in the prequels Event DTs and Epoxy DTs.

    
    public void publishBackground(QMessage message) {
        try {
            publish(message, true, true);
        } catch (Throwable e) {
            throw new NWrappedRuntimeException(e, null, message);
        }
    }
    
    public void publish(QMessage message) throws Throwable {
        publish(message, false, true);
    }
    
    protected boolean publish(QMessage message, boolean background, boolean strict) 
        throws Throwable {
        if (message == null) {
            throw new NullPointerException(configuration.nullMessageError);
        }
        Class messageClass = message.getClass();
        if (!hasSubscriber(messageClass)) {
            if (strict) {
                throw new NRuntimeException(
                    configuration.publishSubscriberListEmptyError, messageClass);
            }
            return false;
        }
        QResponder responder = responderMap.get(messageClass);
        if (responder != null) {
            publisher.publishResponder(responder, message, background);
        }
        publish(defaultSubscriberMap, message, background);
        publish(weakSubscriberMap, message, background);
        return true;
    }
    
    protected boolean hasSubscriber(Class messageClass) {
        if (responderMap.containsKey(messageClass)) return true;
        if (hasSubscriber(defaultSubscriberMap, messageClass)) return true;
        if (hasSubscriber(weakSubscriberMap, messageClass)) return true;
        return false;
    }
    
    protected boolean hasSubscriber(QSubscriberMap subscriberMap, Class messageClass) {
        List<QSubscriber> subscriberList = subscriberMap.getSubscriberList(messageClass).getList();
        return (subscriberList.size() > 0);
    }

In our private publish() message above, if we find that there are no subscribers for our message, and we are being strict, then we complain, bitterly.

We will introduce QSubscriberMap shortly, where this is essentially an alias as follows.

public interface QSubscriberMap extends Map<Class, QSubscriberList> {
}

ie. a Map of message types to the List of subscribers for that message type.

QSubscriberList is essentially an alias as follows, with the addition of a getList() method to provide a copy of its list of subscribers, for thread-safety.

public interface QSubscriberList extends List<QSubscriber> {
    public List<QSubscriber> getList(); // get thread-safe copy
}

We reuse the publish() method below for each QSubscriberMap, namely weakSubscriberMap and defaultSubscriberMap.

    protected void publish(QSubscriberMap subscriberMap, QMessage message, boolean background) 
        throws Throwable {
        Class messageClass = message.getClass();
        List<QSubscriber> subscriberList = subscriberMap.getSubscriberList(messageClass).getList();
        for (QSubscriber subscriber : subscriberList)  {
            publisher.publish(subscriber, message, background);
        }
    }

We use getList() to get a thread-safe copy of the subscriber list, because while we might be waiting for the subcriber to process the message, another thread might invoke the subcribe() and/or unsubscribe() methods for the same message type, which would modify the list.

We delegate to a QPublisher helper as below, to publish the message to a subscriber or responder.

public interface QPublisher<Response> {
    public void publish(
        QSubscriber subscriber, QMessage message, boolean background) 
        throws Throwable;

    public Response publishResponder(
        QResponder responder, QMessage message, boolean background) 
        throws Throwable;
}

Further below we present the QEdtPublisher implementation of the above interface. This invokes the subscriber's receive() method in the EDT using invokeAndWait(), or otherwise in a background SwingWorker thread.

Publishing to Responsive Subscribers

We publish to a responder as follows.

    public Response getResponse(QMessage message) 
        throws Throwable {
        return publishResponder(message, false);
    }
    
    protected Response publishResponder(QMessage message, boolean background) 
        throws Throwable {
        Class messageClass = message.getClass();
        QResponder responder = responderMap.get(messageClass);
        if (responder == null) {
            throw new NRuntimeException(
                configuration.publishResponderNoResponderError, 
                messageClass);
        }
        return publisher.publishResponder(responder, message, background);
    }

where we wait for a response, and return a response object.

So clearly our publishing methods cannot be synchronized, because they might take a while a execute, eg. if background is false.

Spanglish

Incidently, we "externalise" our error messages and such, into a configuration object as follows. Then we can readily inject externalised resources into this object, eg. from a resource bundle.

public class QMessageBusConfiguration {    
    @ResourceAnnotation()
    String nullMessageError = "Cannot publish a null message";                
    
    @ResourceAnnotation()
    String subscribeResponderExclusiveError = "Not exclusive responder";
    
    @ResourceAnnotation()
    String publishSubscriberListEmptyError = "No subscribers for message";            
    
    @ResourceAnnotation()
    String publishResponderNoSubscriberError = "No responder for message";        

    public QMessageBusConfiguration() {
       resourceBundleHelper.configure(this);
    }
}

Subscriber Map

QSubscriberMap maintains a map of QSubscriberList, which manages a list of subscribers, for a given message type.

QSubscriberMap is implemented as follows.

public class QSubscriberMap extends QValueMap<Class, QSubscriberList> {    

    public QSubscriberMap(Class subscriberListClass) {
        super(subscriberListClass);
    }
    
    public QSubscriberList getSubscriberList(Class messageClass) {
        return super.getValue(messageClass);
    }
}

where we extend QValueMap presented below.

public class QValueMap<Key, Value> extends HashMap<Key, Value> {
    Class valueClass;
    
    public QValueMap(Class valueClass) {
        super();
        this.valueClass = valueClass;
    }

    public Value getValue(Key key) {
        Value value = super.get(key);
        if (value == null) {
            try {
                value = (Value) valueClass.newInstance();
                super.put(key, value);
            } catch (Throwable e) {
                throw new NWrappedRuntimeException(e, null, valueClass);
            }
        }
        return value;
    }
}

where we create a value instance on demand in getValue() using valueClass.newInstance().

Default Subscriber List

QSubscriberList below is just an alias for a basic list.

public interface QSubscriberList extends QSimpleList<QSubscriber> {
}

where QSimpleList is defined as follows.

public interface QSimpleList<Element> {    
    public void add(Element element);
    public void remove(Element element);
    public boolean contains(Element element);
    public int size();
    public List<Element> getList();
}

Just to it keep it simple.

We implement QDefaultSubscriberList as follows.

public class QDefaultSubscriberList implements QSubscriberList {
    List<QSubscriber> subscriberList = new ArrayList();
    
    protected QDefaultSubscriberList() {
    }
    
    public synchronized void add(QSubscriber subscriber) {
        subscriberList.add(subscriber);
    }
    
    public synchronized void remove(QSubscriber subscriber) {
        subscriberList.remove(subscriber);
    }
    
    public synchronized boolean contains(QSubscriber subscriber) {
        return subscriberList.contains(subscriber);
    }
    
    public synchronized int size() {
        return subscriberList.size();
    }

    public synchronized List<QSubscriber> getList() {
        return new ArrayList(subscriberList);
    }
}

where we make it thread-safe using synchronized methods.

The getList() method returns a (thread-safe) copy of the list, which we can use to iterate through our subscribers, eg. in a for loop.

Since we have a single responder per message type, we create a regular thread-safe map as follows.

    Map<Class, QResponder> responderMap = Collections.synchronizedMap(new HashMap());

where the key is the message type.

Weak Reference Subscriber List

In addition to the above default subscriber list, we also implement a subscriber list that uses weak references, ie. Java's WeakReference wrapper. In this case, our weak reference to the subscriber will not prevent it being garbage-collected, if there are no other (strong) references to it in our application.

Actually QWeakSubscriberList is just an alias as follows.

public class QWeakSubscriberList extends QWeakList<QSubscriber> {    
}

where QWeakList is implemented as follows.

public class QWeakList<Element> implements QSimpleList<Element> {    
    List<WeakReference> referenceList = new ArrayList();

    public QWeakList() {
    }
    
    public synchronized boolean contains(Element element) {
        for (WeakReference reference : referenceList) {
            if (reference.get() == element) {
                return true;
            }
        }
        return false;
    }
    
    public synchronized void add(Element element) {
        referenceList.add(new WeakReference(element));
    }
    
    public synchronized void remove(Element element) {
        for (WeakReference reference : referenceList) {
            if (reference.get() == element) {
                referenceList.remove(reference);
                break;
            }
        }
    }

    public synchronized List<Element> getList() {
        List<Element> elementList = new ArrayList();
        WeakReference removeReference = null;
        for (WeakReference reference : referenceList) {
            Element element = (Element) reference.get();
            if (element == null) {
                removeReference = reference;
            } else {
                elementList.add(element);
            }
        }
        if (removeReference != null) {
            referenceList.remove(removeReference);
        }
        return elementList;
    }

    public int size() {
        return referenceList.size();
    }
}

In the getList() method, we do some incremental house-keeping, ie. removing a garbage-collected reference, if we find one. We know that if weakReference.get() returns null, then that subscriber has been garbage-collected.

If we publish messages to this list more often than we unsubscribe, which we should do, then it's fine to remove only one garbage-collected reference in getList() at a time. Nonetheless, this is clearly a case of premature optimisation (which we hear is a terrible practice). In particular, we avoid creating a List for possible elements to remove, when there is typically at most one, and usually none.

Receiving messages

QSubscriber is an interface for receiving messages.

public interface QSubscriber<Message> {
    public void receive(Message message) throws Throwable;
}

Our QResponder interface is similar, except that it returns a response, as follows.

public interface QResponder<Message, Response> {
    public Response receive(Message message) throws Throwable;
}

We allow services to throw an exception. In this case, our message bus needs an exception handler. We will implement this in a minute, as a dialog popup service that subscribes to the message bus, to receive exception messages :)

Publishing messages

Our QDefaultMessageBus implementation presented further above delegates to an QPublisher helper to publish messages to subscribers and responders.

public interface QPublisher<Response> {
    public void publish(
        QSubscriber subscriber, QMessage message, boolean background) 
        throws Throwable;
    public Response publishResponder(
        QResponder responder, QMessage message, boolean background) 
        throws Throwable;
}

where QMessage is just a tag as follows.

public interface QMessage {
}

We can request the message bus to invoke the service in the background, or otherwise wait for the execution to complete, eg. using invokeAndWait() on the EDT. In the case of publishResponder(), we can return a response. If publishResponder() is invoked in the background, then it will return null immediately, and it's response is not available in this case.

Publishing to the EDT

We implement an QEdtPublisher, which is cognisant of the EDT. If background is true, we invoke the service in a background SwingWorker thread (which is not the EDT). Otherwise we invokeAndWait() on the EDT.

import static quitebusy.common.QDefaultMessageBus.*; // for messageBus singleton
import static aptfoundation.common.context.NContext.*; // for singletons, eg. logger
...
public class QEdtPublisher<Response> implements QPublisher<Response> {
    
    public void publish(QSubscriber subscriber, QMessage message, boolean background) 
        throws Throwable {
        logger.entering(subscriber, message, background);
        final FutureTask task = new FutureTask(createCallable(subscriber, message));
        if (!background) invokeAndWait(task);
        doInBackground(task);
    }
    
    public Response publishResponder(
        QResponder responder, QMessage message, boolean background) 
        throws Throwable {
        logger.entering(responder, message, background);
        final FutureTask task = new FutureTask(createCallable(responder, message));
        if (!background) return (Response) invokeAndWait(task);
        doInBackground(task);
        return null;
    }
       
    protected Object invokeAndWait(final FutureTask task) throws Throwable {
        logger.entering();
        if (!SwingUtilities.isEventDispatchThread()) {
            SwingUtilities.invokeAndWait(task);
        } else {
            task.run();
        }
        try {
            return task.get();
        } catch (InterruptedException ie) {
            throw ie;
        } catch (ExecutionException ee) {
            throw ee.getCause();
        }
    }
    
    protected void doInBackground(final FutureTask task) {
        logger.entering();
        SwingWorker worker = new SwingWorker() {
            protected Object doInBackground() throws Throwable {
                try {
                    task.run();
                    task.get();
                } catch (Throwable e) {
                    e.printStackTrace();
                    messageBus.publishException(e, null);
                }
                return null;
            }
        };
        worker.execute();
    }
    
    protected Callable createCallable(
        final QSubscriber subscriber, final QMessage message) {
        return new Callable() {
            public Object call() throws Throwable {
                subscriber.receive(message);
                return null;
            }
        };
    }
    
    protected Callable createCallable(
        final QResponder subscriber, final QMessage message) {
        return new Callable() {
            public Object call() throws Throwable {
                return subscriber.receive(message);
            }
        };
    }    
}

In publishResponder(), if we are gonna wait for a response, ie. via invokeAndWait(), rather than doInBackground(), then we can return a response value from our service, eg. a Boolean object from a confirmation dialog service. Otherwise we return null.

In our background SwingWorker, in the event of an error, we publish an exception message to the message bus. We will go into this further below.

Dialogue

So now let's implement some dialog support. First we need a dialog message to publish.

public class QDialogMessage implements QStatusMessage {    
    Component parentComponent = QApplicationContext.getInstance().getFrame(); 
    int option = JOptionPane.OK_OPTION;
    String title;

    public QDialogMessage(String text) {
        super(text);
    }

    ... // getters and setters, eg. getParentComponent(), getOption()
}

where the QStatusMessage superclass is as follows.

public class QStatusMessage implements QMessage {    
    String text;
    
    public QStatusMessage() {
    }
    
    public QStatusMessage(String text) {
        this.text = text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public String getText() {
        return text;
    }
}

Now we can implement a dialog service to receive the above message type.

public class QDialogService implements QSubscriber<QDialogMessage> {     
    public static final QDialogService dialogService = new QDialogService();
    
    private QDialogService() {    
    }    

    public void receive(QDialogMessage message) throws Throwable {
        JOptionPane.showMessageDialog(
            message.getParentComponent(), 
            message.getText());
    }    
}

where we create a singleton, so that it is impossible to create and subscribe more than one dialog service by mistake, in which case, multiple dialogs would popup at the same time.

(*)

Two-way Dialogue

We implement a QConfirmationDialogService as follows, to handle confirmation dialogs, so it is a responder service, returning a Boolean response.

public class QConfirmationDialogService 
        implements QResponder<QConfirmationMessage, Boolean> { 
    ...    
    public Boolean receive(QConfirmationMessage message) throws Throwable {
        int result = JOptionPane.showConfirmDialog(
            message.getParentComponent(), 
            message.getText(), 
            null, 
            message.getOption());
        return result == message.getConfirmationOption();
    }    
}

where QConfirmationMessage extends QDialogMessage as follows.

public class QConfirmationMessage extends QDialogMessage {
    int option = JOptionPane.OK_CANCEL_OPTION;
    int confirmationOption = JOptionPane.OK_OPTION;
    
    public QConfirmationMessage(String message) {
        super(message);
        setOption(option);
    }
    
    public int getConfirmationOption() {
        return confirmationOption;
    }
}

where our confirmation message has an affirmative confirmationOption set to JOptionPane.OK_OPTION. We might also want to add support for YES_NO_OPTION, as an alternative.

Kicking the tyres

Let's see if it works, by subscribing an instance of QDialogService to the bus, and then publishing a QDialogMessage.

public class QMessageBusDemo {    
    ...

    protected void subscribe() {   
        messageBus.subscribe(QDialogMessage.class, QDialogService.dialogService);
        ...      
    }

    public void testMessageDialog() {
        messageBus.publish(new QDialogMessage("Hello, Emission DT!"));
    }
}

Here again is Web Start demo of this Quite Busy warez.

(*) (QuiteBusy, Java5, 450k, unsandboxed)

Which looks something like this...

(*)

Status Bar

Let's implement a QInfoMessage, which we might use for logging, so it has a verbosity Level.

public class QInfoMessage extends QStatusMessage {
    Level level;

    public QInfoMessage(Level level, String text) {
        super(text);
        this.level = level;
    }

    public Level getLevel() {
        return level;
    }    
}

We can implement a trivial logging service as follows.

public class QLoggerInfoService implements QSubscriber<QInfoMessage> {

    public void receive(QInfoMessage message) throws Throwable {
        logger.logp(message.getLevel(), message.getText());
    }
}

which uses our static logger. Incidently, we can control the verbosity per class/package of this static logger via command-line parameters, using some tricks - see the upcoming Plumber's Hack: At Loggerheads.

Let's implement a status bar service, which updates a JLabel with the info message, and changes the background color, eg. to highlight a warning message.

public class QStatusBarInfoService implements QSubscriber<QInfoMessage> {
    JPanel labelPanel;
    JLabel label;
    
    public QStatusBarInfoService(JLabel label, JPanel labelPanel) {
        this.label = label;
        this.labelPanel = labelPanel;
    }
    
    public void receive(QInfoMessage message) throws Throwable {
        label.setText(message.getText());
        if (message.getLevel() == Level.SEVERE) {
            labelPanel.setBackground(Color.orange);
        } else if (message.getLevel() == Level.WARNING) {
            labelPanel.setBackground(Color.yellow);
        } else {
            labelPanel.setBackground(Color.white);
        }
    }
}

where since a JLabel's background is transparent, we put the JLabel into a JPanel, and change the background of the JPanel.

We try it out as follows.

public class QMessageBusDemo {    
    JLabel statusLabel = new JLabel();
    JPanel statusLabelPanel = new JPanel(new GridBagLayout());
    ...
    protected void subscribe() {   
        statusLabelPanel.add(statusLabel, Gbc.xy(0, 0).insets(4).both());
        messageBus.subscribe(QInfoMessage.class, 
            new QStatusBarInfoService(statusLabel, statusLabelPanel));
        ...
    }

    public void testInfo() {
        messageBus.publish(new QInfoMessage(Level.INFO, "This is a info message"));
    }

    public void testWarning() {
        messageBus.publish(new QInfoMessage(Level.WARNING, "This is an warning message"));
    }
    ...
}

where Gbc was presented in Grid Bag Grease.

Our message bus can now use this for a publishInfo() method below as below, which we provide for convenience.

public class QDefaultMessageBus<Response> implements QMessageBus<Response> {
    ...
    public void publishInfo(Level level, String text) {
        QInfoMessage infoMessage = new QInfoMessage(level, text);
        try {
            if (publish(infoMessage, false, false)) return;
        } catch (Throwable e) {
            logger.warning(e);
        }
        logger.logp(level, text);
    }
}

where if no service has been subscribed to QInfoMessage, then publish() returns false (rather than throwing an exception, since our strict parameter is false), in which case we use our regular logger.

Consoling

We implement a service to append info messages to a JTextPane console as follows.

public class QTextPaneConsoleInfoService implements QSubscriber<QInfoMessage> {
    JTextPane textPane;

    public QTextPaneConsoleInfoService(JTextPane textPane) {
        this.textPane = textPane;
    }
    
    public void receive(QInfoMessage message) throws Throwable {
        if (message.getText() == null) return;
        String text = textPane.getText();
        StringBuffer buffer = new StringBuffer();
        Level level = message.getLevel();
        if (level != null) buffer.append(level.toString() + ": ");
        buffer.append(message.getText());
        textPane.setText(text + "\n" + buffer.toString());
    }
}

which we test as follows.

public class QMessageBusDemo {    
    JTextPane consoleTextPane = new JTextPane();
    ...
    protected void subscribe() {   
        messageBus.subscribe(QInfoMessage.class, 
            new QTextPaneConsoleInfoService(consoleTextPane));
        messageBus.subscribe(QDialogMessage.class, 
            new QTextPaneConsoleInfoService(consoleTextPane));
        ...
    }

    public void testInfo() throws Throwable {
        messageBus.publish(new QInfoMessage(Level.WARNING, "This is an warning message"));
        messageBus.publishInfo(Level.FINE, "This is one fine message"); // convenience method
    }
}

where if we make QDialogMessage extend QInfoMessage as follows, then we can subscribe our text pane console service to dialog messages too, as above, so that those are also displayed in our consoleTextPane.

public class QDialogMessage extends QInfoMessage {    
    ...    
    public QDialogMessage(String text) {
        super(Level.INFO, text);
    }

    public QDialogMessage(Level level, String text) {
        super(level, text);
    }
    ...    
}

where we choose our default verbosity to be at the INFO level, for dialog messages.

Exceptional Dialogue

Our QExceptionMessage could extend the above QDialogMessage, to include an exception.

public class QExceptionMessage extends QDialogMessage {    
    Exception exception;

    public QExceptionMessage(Exception exception, String message) {
        super(formatter.formatExceptionMessage(exception, message));
        this.exception = exception;
    }
    
    public Exception getException() {
        return exception;
    }   
}

Our message bus can now use this message in its publishException() convenience method below, eg. to publish exceptions that might occur in QEdtPublisher.doInBackground() ie. thrown by a service we invoked in the background.

    public void publishException(Throwable exception, String text) {
        QExceptionMessage exceptionMessage = new QExceptionMessage(exception, text);
        try {
            if (publish(exceptionMessage, false, false)) return;
        } catch (Throwable e) {
            logger.warning(e);
        }
        dialogHelper.showExceptionDialog(exception, text);
    }

where we revert to our dialogHelper if there are no subscribers for QExceptionMessage.

We can then subscribe a QDialogService instance as follows, eg. to popup exception messages that might occur in background SwingWorker threads, which will otherwise be handled by dialogHelper.showExceptionDialog() as above.

    public void testException() {    
        messageBus.subscribe(QExceptionMessage.class, new QDialogService());
        messageBus.publishException(
                new NullPointerException("drill too small"), 
                "error occurred while fixing camera"
                ));
    }

But let's rather implement a QExceptionDialogService, to popup an exception panel with a "Details" button to show the stack trace.

public class QExceptionDialogService implements QSubscriber<QExceptionMessage> {       

    public void receive(QExceptionMessage message) throws Throwable {
        QExceptionPanel panel = new QExceptionPanel(
            message.getException(), message.getMessageText());
        JDialog dialog = panel.createDialog(
            QApplicationData.getInstance().getFrame());
        dialog.setVisible(true);
    }
}

(*)

Exceptions Everywhere

If we wanna display exceptions in our status bar, we can make QExceptionMessage extend QInfoMessage as follows

public class QExceptionMessage extends QInfoMessage {
    Throwable exception;
    
    public QExceptionMessage(Throwable exception, String text) {
        super(Level.SEVERE, text);
        this.exception = exception;
    }
    
    public Throwable getException() {
        return exception;
    }   
}

Now we can subscribe the status bar service presented further above, to exception messages as follows. Then, when an exception gets published on the bus, it will be displayed in our status bar, as well as in an exception panel, if we have subscribed that service as well.

public class QMessageBusDemo {    
    JLabel statusLabel = new JLabel();
    JPanel statusLabelPanel = new JPanel(new GridBagLayout());
    ...
    protected void subscribe() {   
        messageBus.subscribe(QExceptionMessage.class, new QExceptionDialogService());
        messageBus.subscribe(QExceptionMessage.class, 
            new QStatusBarInfoService(statusLabel, statusLabelPanel));
        ...
    }

Of course, we probably wanna log our exceptions and stack traces to our JTextPane console introduced earlier.

public class QInfoExceptionService implements QSubscriber<QExceptionMessage> {
    public void receive(QExceptionMessage message) throws Exception {
        String text = formatter.formatExceptionStackTrace(
                message.getException(), 
                message.getText());
        messageBus.publishInfo(
                message.getLevel(),
                text);
    }
}

where we publish a QInfoMessage message courtesy of the publishInfo() convenience method, and that will get picked up by our QTextPaneConsoleInfoService.

So we subscribe to info and exception messages as follows.

public class QMessageBusDemo {    
    JTextPane consoleTextPane = new JTextPane();
    ...
    protected void subscribe() {   
        messageBus.subscribe(QInfoMessage.class, new QStatusBarInfoService(...));
        messageBus.subscribe(QInfoMessage.class, new QTextPaneConsoleInfoService(...));
        messageBus.subscribe(QExceptionMessage.class, new QStatusBarInfoService(...));
        messageBus.subscribe(QExceptionMessage.class, new QInfoExceptionService());
        messageBus.subscribe(QExceptionMessage.class, new QExceptionDialogService());
        ...
    }
    ...

where we subscribe both status bar and console display services, so that when we publishInfo() and publishException(), those will show in the status bar and console. In the case of QExceptionMessage, we will popup QExceptionPanel as well, courtesy of the QExceptionDialogService.

Progressive Dialogue

Finally, let's implement a "progress panel" with a JProgressBar. Then we can implement a progress service, and a message to publish to it.

public class QProgressPanel extends JPanel implements ActionListener {
    JDialog dialog = new JDialog();
    JProgressBar progressBar = new JProgressBar(0, 100);
    JLabel messageLabel = new JLabel();
    JButton cancelButton = new JButton(configuration.cancelButton);
    ...
    
    public void setProgress(Integer progress) {
        if (progress == null) return;
        progressBar.setValue(progress);
        progressLabel.setText(formatter.format(configuration.progressFormat, progress));
    }
    ...    
}

(*)

Let's implement a progress message, for a progress service.

public class QProgressMessage extends QStatusMessage {    
    Integer progress;
    QProgressStatus status;
    String detailText;
            
    public QProgressMessage() {
    }
    
    ... // getters and setters
}

Now we can implement the service to receive the above progress message.

public class QProgressDialogService implements QSubscriber<QProgressMessage> {
    QProgressPanel panel = new QProgressPanel();
    JDialog dialog = panel.createDialog(QApplicationContext.getInstance().getFrame());
    QProgressDialogServiceConfiguration configuration = 
        new QProgressDialogServiceConfiguration();
    
    public void receive(QProgressMessage message) throws Throwable {
        panel.setStatus(message.getStatus());
        if (panel.isCancelled()) {
            throw new QCancelledException(configuration.cancelledByUser);
        }
        panel.setMessage(message.getText());
        panel.setDetailText(message.getDetailText());
        panel.setProgress(message.getProgress());
        if (!dialog.isVisible()) {
            dialog.setVisible(true);
        }
    }
}

where if the user has cancelled the process, ie. by hitting the "Cancel" button on the progress panel, then we throw a QCancelledException when we receive a progress update message.

Let's test it.

    public void testProgress() {
        messageBus.subscribe(QProgressMessage.class, new QProgressDialogService());
        messageBus.subscribe(QProgressDemoMessage.class, new QProgressDemoService());
        messageBus.publishBackground(new QProgressDemoMessage());
    }

where QProgressDemoService is implemented as below. Because this is a long task, it must be executed in a SwingWorker thread to avoid blocking the EDT. Therefore we must launch this service using publishBackground(), as above. It receives a QProgressDemoMessage, which is just an empty class tagged with the empty QMessage interface.

public class QProgressDemoService implements Runnable,
        QMessage, QSubscriber<QProgressDemoMessage> {

    JFrame applicationFrame = QApplicationContext.getInstance().getFrame();
    JPanel glassPane = new JPanel();
    
    public QProgressDemoService() {
        glassPane.setOpaque(false);
        glassPane.setVisible(false);
        glassPane.addMouseListener(new MouseAdapter() {});
        applicationFrame.setGlassPane(glassPane);
    }
    
    public void receive(QProgressDemoMessage message) throws Throwable {
        run();
    }

    public void run() {
        setWaitCursor(true);
        try {
            messageBus.publish(createProgressStatus(QProgressStatus.startedProgressStatus));
            for (int i = 80; i < 100; i += 4) {
                try {
                    Thread.sleep(1000);
                } catch (Throwable e) {
                }
                messageBus.publish(createProgress(i));
                if (i % 8 == 0) {
                    messageBus.publish(createProgressMessageText("Fetching data for the %d-th time", i));
                }
                messageBus.publish(createProgressDetail("Iteration %d", i));
            }
            messageBus.publish(createProgressStatus(QProgressStatus.doneProgressStatus));
        } catch (QCancelledException e) {
            messageBus.publishInfo(Level.INFO, "Our long process was cancelled by the user");
        } catch (Throwable e) {
            messageBus.publishException(e, "An error occurred in our long process");
        } finally {
            setWaitCursor(false);
        }
    }
    ...
}

In our run() method above, we activate the wait cursor using the setWaitCursor() method below, and are sure to restore it in the finally clause.

    protected void setWaitCursor(final boolean waitCursor) {
        try {
            SwingUtilities.invokeAndWait(new Runnable() {
                public void run() {
                    if (!waitCursor) applicationFrame.setCursor(null);
                    else applicationFrame.setCursor(
                        Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
                    glassPane.setVisible(waitCursor);
                }
            });
        } catch (Throwable e) {
            throw new NWrappedRuntimeException(e, null);
        }
    }

where since this service runs outside the EDT, we use invokeAndWait() in our setWaitCursor() method.

In addition to the wait cursor, we activate a glass pane over our application frame, to block any mouse events, eg. clicking on buttons. This forces the user to wait for the process to complete, or otherwise cancel the process using the progress dialog. In this case, we will catch a QCancelledException in the run() method further above.

We use convenience methods to create instances of QProgressMessage, for example as follows.

    protected QProgressMessage createProgress(int progress) {
        QProgressMessage message = new QProgressMessage();
        message.setProgress(progress);
        return message;
    }

where other fields in the progress message are left as null, eg. in the above case only the progress is set, so detailText and status are null. It might be clearer to implement multiple progress messages, eg. QProgressStatusMessage, QProgressDetailMessage and QProgressPercentageMessage.

Summary
"Mmmm, donuts - is there anything they can't do?" Homer Simpson

As an exercise in understanding the basics of eventbus.dev.java.net, we implement a simplistic publish/subscribe bus.

EventBus provides a mechanism for the loose-coupling of components. Swing listeners need to be attached to specific components, whereas subscribers and publishers only need to know about the Bus, and not each other.

The EventBus mechanism can be used to address EDT issues. Data, actions and such, can be be moved between EDT and non-EDT threads using the bus, for execution in the EDT, or in the background, as appropriate.

Carrying On

This article might be followed up with Quite Busy Userage, to use the message bus to implement the following user-related services in a loosely-coupled fashion.

  • a logon dialog service.
  • a service to authentificate a user logon.
  • a distributed message bus, eg. to authentificate users on a remote application server.
  • a service for populating and enabling menus for users when they logon, and disabling the same when they logoff.
  • publishing context-relevant help along the way, ready if asked for by the user.
  • publishing artifacts on the bus for configuration by resources, should those be available in our Resource Bundle, eg. the translation of menu labels, help messages and such, into the user's preferred language.
  • publishing artifacts on the bus for configuration by user preferences, should those be available, eg. the size and position of the application frame, table columns, split pane dividers and such, which we might save using the Preferences API.

Here is that Web Start demo again...

(*) (QuiteBusy, Java5, 450k, unsandboxed)

Incidently, this QuiteBusy article was written with the help of Quitewriter, which also performs the the source code highlighting in the above demo, on the fly. Plumber's Hack 2: Quitewriter has been queued up for publication for some weeks now, so hopefully i'll get around to final review and publication of that next week. Followed the week after by QuiteGooey, which was an exercise towards a minimal GUI framework - the only problem is that it hasn't turned out to be as minimal as i planned, D'oh! The above demo uses a slimmed down superclass called QuiteBasicGooey, which is trivial. Still the jar is unfortunately currently 450k (and growing) cos it's got Quitewriter and QuiteGooey bundled in there (270 classes and counting). Apologies for that. I will be expending some effort to split it up at some point, so...


Bookmark blog post: del.icio.us del.icio.us Digg Digg DZone DZone Furl Furl Reddit Reddit
Comments
Comments are listed in date ascending order (oldest first) | Post Comment





Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds