DecoupledEventProcessor.java
package swingtree.threading;
import org.slf4j.Logger;
import swingtree.UI;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* This is a synchronized singleton wrapping a {@link BlockingQueue}.
*/
public final class DecoupledEventProcessor implements EventProcessor
{
private static final Logger log = org.slf4j.LoggerFactory.getLogger(DecoupledEventProcessor.class);
private static final DecoupledEventProcessor _INSTANCE = new DecoupledEventProcessor();
static DecoupledEventProcessor INSTANCE() { return _INSTANCE; }
/**
* This is a simple queue for pending event calls which is thread safe and allows the
* GUI thread to register application events.
*/
private final BlockingQueue<Runnable> rendererQueue = new LinkedBlockingQueue<>();
@Override public void registerAppEvent(Runnable task ) {
try {
rendererQueue.put(task);
} catch (Exception e) {
log.error("Failed to register application event!", e);
}
}
@Override
public void registerAndRunAppEventNow(Runnable runnable ) {
// We add the task to the queue and then wait for it to be processed.
// This is a blocking call.
boolean[] done = new boolean[1];
try {
rendererQueue.put(() -> {
runnable.run();
synchronized (done) {
done[0] = true;
done.notifyAll();
// notify the waiting thread, meaning the GUI/frontend thread,
// which is waiting for the task to be processed.
}
});
synchronized (done) {
while ( !done[0] ) done.wait();
// wait for the task to be processed. The wait is released by the notifyAll() call in the task.
}
} catch (Exception e) {
log.error("Failed to register and run application event!", e);
}
}
@Override
public void registerUIEvent(Runnable runnable) {
UI.run(runnable);
}
@Override
public void registerAndRunUIEventNow(Runnable runnable) {
try {
UI.runNow(runnable);
} catch (Exception e) {
log.error("Failed to register and run UI event!", e);
}
}
/**
* This method is called by a thread to process all GUI events, this should be the application's main thread.
* @param rethrow If true, any exception thrown by the event handler will be rethrown.
* @throws InterruptedException If the thread is interrupted while waiting for the event to be processed.
* Only thrown if {@code rethrow} is true.
*/
void join( boolean rethrow ) throws InterruptedException {
while ( true ) {
try {
this.rendererQueue.take().run();
} catch (Exception e) {
if (rethrow)
throw e;
else
log.error("An exception occurred while processing an event!", e);
}
}
}
/**
* A fully blocking call to the decoupled thread event processor
* causing this thread to join its event queue
* so that it can continuously process events produced by the UI.
* <p>
* This method wither be called by the main thread of the application
* after the UI has been built and shown to the user, or alternatively
* a new thread dedicated to processing events. (things like button clicks, etc.)
* @throws IllegalStateException If this method is called from the UI thread.
*/
public void join() {
if ( UI.thisIsUIThread() )
throw new IllegalStateException("The UI thread cannot join the application event processing queue!");
try {
this.join(false);
} catch (InterruptedException e) {
log.error("The application event processing queue was interrupted!", e);
}
}
/**
* A fully blocking call to the decoupled thread event processor
* causing this thread to join its event queue
* so that it can continuously process events produced by the UI.
* <p>
* This method should be called by the main thread of the application
* after the UI has been built and shown to the user, or alternatively
* a new thread dedicated to processing events. (things like button clicks, etc.)
* <p>
* This method will block until an exception is thrown by the event processor.
* This is useful for debugging purposes.
* @throws InterruptedException If the thread is interrupted while waiting for the event processor to join.
*/
public void joinUntilException() throws InterruptedException {
this.join(true);
}
/**
* A fully blocking call to the decoupled thread event processor
* causing this thread to join its event queue
* so that it can process the given number of events produced by the UI.
* <p>
* This method should be called by the main thread of the application
* after the UI has been built and shown to the user, or alternatively
* a new thread dedicated to processing events. (things like button clicks, etc.)
* <p>
* This method will block until the given number of events have been processed.
* @param numberOfEvents The number of events to wait for.
*/
public void joinFor( long numberOfEvents ) {
for ( long i = 0; i < numberOfEvents; i++ ) {
try {
this.rendererQueue.take().run();
} catch (Exception e) {
log.error("An exception occurred while processing an event!", e);
}
}
}
/**
* A temporarily blocking call to the decoupled thread event processor
* causing this thread to join its event queue
* so that it can process the given number of events produced by the UI.
* <p>
* This method should be called by the main thread of the application
* after the UI has been built and shown to the user, or alternatively
* a new thread dedicated to processing events. (things like button clicks, etc.)
* <p>
* This method will block until the given number of events have been processed
* or an exception is thrown by the event processor.
* @param numberOfEvents The number of events to wait for.
* @throws InterruptedException If the thread is interrupted while waiting for the event processor to join.
*/
public void joinUntilExceptionFor( long numberOfEvents ) throws InterruptedException {
for ( long i = 0; i < numberOfEvents; i++ )
this.rendererQueue.take().run();
}
/**
* A temporarily blocking call to the decoupled thread event processor
* causing this thread to join its event queue
* so that it can continuously process events produced by the UI
* until all events have been processed or an exception is thrown by the event processor.
* <p>
* This method should be called by the main thread of the application
* after the UI has been built and shown to the user, or alternatively
* a new thread dedicated to processing events. (things like button clicks, etc.)
* @throws InterruptedException If the thread is interrupted while waiting.
*/
public void joinUntilDoneOrException() throws InterruptedException {
while ( !this.rendererQueue.isEmpty() )
this.rendererQueue.take().run();
}
}