How do I redispatch ANY AWTEvent in java?
I have a feature to implement in a Java 1.5 Swing based application. If a particular exception happens when processing an AWTEvent, I have to popup an alternate form, resolve the problem and continue processing the original event. When I redispatch the event to the component nothing happens. When I push the event into the event queue nothing happens. I assume there is some state fields in the event which mark it as processed, so components don't pick it up. So far I cannot find a way to recreate a clone of the event. And a custom event will not help here because I want the previous event to get processed.
In the swing application, the existing event queue gets replaced by an internal queue.
private class ApplicationEventQueue extends EventQueue
{
private final ArrayList listeners=new ArrayList();
protected void initialize()
{
Toolkit.getDefaultToolkit().getSystemEventQueue().push(this);
}
.
.
.
}
As part of the dispatch event, The class intercepts the call ans delegates to the super class. If an exception occurs it will pop-up a messagebox with a "sorry for the inconvienience" message.
@Override
protected void dispatchEvent(AWTEvent event)
{
try
{
super.dispatchEvent(event);
if (peekEvent() != null && userEventDispatched)
{
raiseIdleEvent();
userEventDispatched = false;
}
else
{
int eventId = event.getID();
if (eventId == KeyEvent.KEY_TYPED || eventId == MouseEvent.MOUSE_CLICKED)
{
userEventDispatched = true;
}
}
}
catch (Throwable ex)
{
onError(ex);
}
}
The feature required is to be able to timeout a user session. The server will throw a specific exception when the session times out. On timeout the user is prompted to relogin and the original action that was aborted will proceed. What I wanted to do is, As part of the onError I will handle the exception by displaying a form. That particular event would be consumed but after re-authentication, I can redispatch the same event to the Application or perhaps push it into the event queue. Both approaches have failed since I presume the event has flags that indicated whether it was posted and consumed.
- The event coming through could be any event (whether mouse of a keystroke).
- Defining a custom event would not solve them problem since I need to replay the same event.
- I have considered cloning the event but cloning is not supported by AWTEvent.
- Deep copy by serializing and then de-serializing the event did not work as some events 开发者_如何学Cbeing dispatched are not serializable.
- I am considering resetting any state variables in the event by reflection but it seems dangerous.
Sorry for the lousy formatting, I haven't figured out the markup yet. Any help here would be appreciated.
FIXED: Thank you for all the answers. The fix (not found by myself) was to catch the session timeout exception when the call was made. The application popped up a dialog and asked the user to re-authenticate. After the authentication was successful, the dialog was closed. It worked to my surprise.
I do not know for certain but it seems like the event remained stuck in the queue when the dialog was shown and once the dialog was closed, it got delivered to the controls it was going to be anyway.
I would not try to solve this from an event perspective. The event system isn't intended to work that way.
I would define an interface (X) that encapsulates interactions with the server. The implementation, Y, would save the parameters of the last server request. After a timeout occurs, and the user has re-authenticated, then I can ask Y to resend the last request.
Extra benefit: since X is an interface, this simplifies testing as Y can be replaced with a mock object to test the GUI, and test code can call Y without a GUI present.
Update:
Here is an approach that uses SwingWorker to run the server interaction on a bg thread. A ChangeEvent is used to return the results to the EDT for processing. SwingWorker's publish/process are used to handle the user interaction for re-auth. A benefit of SwingWorker is that should the server take a long time to respond, the UI can still respond to repaint events.
class Test extends JPanel {
private JButton b;
public Test() {
b = new JButton(new AbstractAction("Do something") {
@Override
public void actionPerformed(ActionEvent e) {
final JButton btn = (JButton) e.getSource();
btn.setEnabled(false);
Object req = new Object(); // Replace w/ apropriate type
new RequestDispatch(req, new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
final Object req = e.getSource();
// Do something with data from 'req'
btn.setEnabled(true);
}
});
}
});
add(b);
}
}
class RequestDispatch extends SwingWorker<Object, Void> {
private enum DispatchState { Ready, Running, Canceled }
private final ChangeListener listener;
private DispatchState dstate = DispatchState.Ready;
private final Object req;
RequestDispatch(Object req, ChangeListener listener)
{
this.req = req;
this.listener = listener;
execute();
}
@Override
protected Object doInBackground() throws Exception {
while (true) {
DispatchState st = getDState();
if (st == DispatchState.Ready)
{
try {
setDstate(DispatchState.Running);
// send request to the server, update req with response
return req;
}
catch (TimeoutException ex) {
this.publish((Void)null);
synchronized (this) {
wait();
}
}
}
if (st == DispatchState.Canceled) {
return req;
}
}
}
@Override
protected void process(List<Void> chunks) {
// Session has timed out
// Ask the user to re-authenticate
int result = JOptionPane.showConfirmDialog(null, "Login");
if (result == JOptionPane.CANCEL_OPTION) {
setDstate(DispatchState.Canceled);
}
else {
setDstate(DispatchState.Ready);
}
}
private synchronized DispatchState getDState() {
return dstate;
}
private synchronized void setDstate(DispatchState dstate) {
this.dstate = dstate;
notifyAll();
}
}
You could put the original event into a custom wrapper class with a name like AWTEventWrapper
which extends AWTEvent
. This class then then overrides all methods and delegates them to the wrapped event, except for consume()
and isConsumed()
(leave the default implementations) so that you can give the illusion that the event is not consumed, and allow it to be processed one more time.
Looking at JDK6 code, AWTEvent.consumed
cannot be set to false as there's only consume()
, no unconsume()
.
I think OP's own #5 is the only way, though indeed dangerous. (I actually just learned how to set a private field from AWTEvent#get_InputEvent_CanAccessSystemClipboard()
.)
In your onError()
, you can create another event queue that overrides getNextEvent
plus a boolean flag, so that when you call EventQueue#push()
the new queue will keep all current events in a list, then do your error notification, finally pop the new queue, flip consumed
in the event that threw the exception, redispatch it, followed by the earlier events you saved in the list.
精彩评论