--- /dev/null
+package javajs.async;
+
+import java.awt.Component;
+
+import javax.swing.ProgressMonitor;
+import javax.swing.SwingUtilities;
+import javax.swing.SwingWorker;
+
+import javajs.async.SwingJSUtils.StateHelper;
+import javajs.async.SwingJSUtils.StateMachine;
+
+/**
+ * v. 2020.06.03
+ *
+ * Executes synchronous or asynchronous tasks using a SwingWorker in Java or
+ * JavaScript, equivalently.
+ *
+ * Unlike a standard SwingWorker, AsyncSwingWorker may itself be asynchronous.
+ * For example, it might load a file asynchronously, or carry out a background
+ * process in JavaScript much like one might be done in Java, but with only a
+ * single thread.
+ *
+ * Whereas a standard SwingWorker would execute done() long before the
+ * asynchronous task completed, this class will wait until progress has been
+ * asynchronously set greater or equal to its max value or the task is canceled
+ * before executing that method.
+ *
+ * Three methods must be supplied by the subclass:
+ *
+ * void initAsync()
+ *
+ * int doInBackgroundAsync(int progress)
+ *
+ * void doneAsync()
+ *
+ * Both initAsync() and doneAsync() are technically optional - they may be
+ * empty. doInBackgroundAsync(), however, is the key method where, like
+ * SwingWorker's doInBackground, the main work is done. The supplied progress
+ * parameter reminds the subclass of where it is at, and the return value allows
+ * the subclass to update the progress field in both the SwingWorker and the
+ * ProgressMonitor.
+ *
+ * If it is desired to run the AsyncSwingWorker synchronously, call the
+ * executeSynchronously() method rather than execute(). Never call
+ * SwingWorker.run().
+ *
+ * Note that doInBackgroundAsync runs on the Java AWT event queue. This means
+ * that, unlike a true SwingWorker, it will run in event-queue sequence, after
+ * anything that that method itself adds to the queue. This is what SwingWorker itself
+ * does with its done() signal.
+ *
+ * If doInBackgroundAsync has tasks that are time intensive, the thing to do is to
+ *
+ * (a) pause this worker by setting the value of progress for the NEXT step:
+ *
+ * setProgressAsync(n);
+ *
+ * (b) pause the timer so that when doInBackgroundAsync returns, the timer is not fired:
+ *
+ * setPaused(true);
+ *
+ * (c) start your process as new Thread, which bypasses the AWT EventQueue:
+ *
+ * new Thread(Runnable).start();
+ *
+ * (d) have your thread, when it is done, return control to this worker:
+ *
+ * setPaused(false);
+ *
+ * This final call restarts the worker with the currently specified progress value.
+ *
+ * @author hansonr
+ *
+ */
+public abstract class AsyncSwingWorker extends SwingWorker<Void, Void> implements StateMachine {
+
+
+ // PropertyChangeEvent getPropertyName()
+
+ private static final String PROPERTY_STATE = "state";
+ private static final String PROPERTY_PAUSE = "pause";
+
+ // PropertyChangeEvent getNewValue()
+
+ public static final String STARTED_ASYNC = "STARTED_ASYNC";
+ public static final String STARTED_SYNC = "STARTED_SYNC";
+
+ public static final String DONE_ASYNC = "DONE_ASYNC";
+ public static final String CANCELED_ASYNC = "CANCELED_ASYNC";
+
+ public static final String PAUSED = "PAUSED";
+ public static final String RESUMED = "RESUMED";
+
+ protected int progressAsync;
+
+ /**
+ * Override to provide initial tasks.
+ */
+ abstract public void initAsync();
+
+ /**
+ * Given the last progress, do some portion of the task that the SwingWorker
+ * would do in the background, and return the new progress. returning max or
+ * above will complete the task.
+ *
+ * @param progress
+ * @return new progress
+ */
+ abstract public int doInBackgroundAsync(int progress);
+
+ /**
+ * Do something when the task is finished or canceled.
+ *
+ */
+ abstract public void doneAsync();
+
+ protected ProgressMonitor progressMonitor;
+
+ protected int delayMillis;
+ protected String note;
+ protected int min;
+ protected int max;
+ protected int progressPercent;
+
+ protected boolean isAsync;
+ private Exception exception;
+
+ /**
+ * Construct an asynchronous SwingWorker task that optionally will display a
+ * ProgressMonitor. Progress also can be monitored by adding a
+ * PropertyChangeListener to the AsyncSwingWorker and looking for the "progress"
+ * event, just the same as for a standard SwingWorker.
+ *
+ * @param owner optional owner for the ProgressMonitor, typically a JFrame
+ * or JDialog.
+ *
+ * @param title A non-null title indicates we want to use a
+ * ProgressMonitor with that title line.
+ *
+ * @param delayMillis A positive number indicating the delay we want before
+ * executions, during which progress will be reported.
+ *
+ * @param min The first progress value. No range limit.
+ *
+ * @param max The last progress value. No range limit; may be greater
+ * than min.
+ *
+ */
+ public AsyncSwingWorker(Component owner, String title, int delayMillis, int min, int max) {
+ if (title != null && delayMillis > 0) {
+ progressMonitor = new ProgressMonitor(owner, title, "", Math.min(min, max), Math.max(min, max));
+ progressMonitor.setProgress(Math.min(min, max)); // displays monitor
+ }
+ this.delayMillis = Math.max(0, delayMillis);
+ this.isAsync = (delayMillis > 0);
+
+ this.min = min;
+ this.max = max;
+ }
+
+ public void executeAsync() {
+ firePropertyChange(PROPERTY_STATE, null, STARTED_ASYNC);
+ super.execute();
+ }
+
+ public void executeSynchronously() {
+ firePropertyChange(PROPERTY_STATE, null, STARTED_SYNC);
+ isAsync = false;
+ delayMillis = 0;
+ try {
+ doInBackground();
+ } catch (Exception e) {
+ exception = e;
+ e.printStackTrace();
+ cancelAsync();
+ }
+ }
+
+ public Exception getException() {
+ return exception;
+ }
+
+ public int getMinimum() {
+ return min;
+ }
+
+ public void setMinimum(int min) {
+ this.min = min;
+ if (progressMonitor != null) {
+ progressMonitor.setMinimum(min);
+ }
+ }
+
+ public int getMaximum() {
+ return max;
+ }
+
+ public void setMaximum(int max) {
+ if (progressMonitor != null) {
+ progressMonitor.setMaximum(max);
+ }
+ this.max = max;
+ }
+
+ public int getProgressPercent() {
+ return progressPercent;
+ }
+
+ public void setNote(String note) {
+ this.note = note;
+ if (progressMonitor != null) {
+ progressMonitor.setNote(note);
+ }
+ }
+
+ /**
+ * Cancel the asynchronous process.
+ *
+ */
+ public void cancelAsync() {
+ helper.interrupt();
+ }
+
+ /**
+ * Check to see if the asynchronous process has been canceled.
+ *
+ * @return true if StateHelper is not alive anymore
+ *
+ */
+ public boolean isCanceledAsync() {
+ return !helper.isAlive();
+ }
+
+ /**
+ * Check to see if the asynchronous process is completely done.
+ *
+ * @return true only if the StateMachine is at STATE_DONE
+ *
+ */
+ public boolean isDoneAsync() {
+ return helper.getState() == STATE_DONE;
+ }
+
+ /**
+ * Override to set a more informed note for the ProcessMonitor.
+ *
+ * @param progress
+ * @return
+ */
+ public String getNote(int progress) {
+ return String.format("Completed %d%%.\n", progress);
+ }
+
+ /**
+ * Retrieve the last note delivered by the ProcessMonitor.
+ *
+ * @return
+ */
+ public String getNote() {
+ return note;
+ }
+
+ public int getProgressAsync() {
+ return progressAsync;
+ }
+
+ /**
+ * Set the [min,max] progress safely.
+ *
+ * SwingWorker only allows progress between 0 and 100. This method safely
+ * translates [min,max] to [0,100].
+ *
+ * @param n
+ */
+ public void setProgressAsync(int n) {
+ n = (max > min ? Math.max(min, Math.min(n, max)) : Math.max(max, Math.min(n, min)));
+ progressAsync = n;
+ n = (n - min) * 100 / (max - min);
+ n = (n < 0 ? 0 : n > 100 ? 100 : n);
+ progressPercent = n;
+ }
+
+ ///// the StateMachine /////
+
+ private final static int STATE_INIT = 0;
+ private final static int STATE_LOOP = 1;
+ private final static int STATE_WAIT = 2;
+ private final static int STATE_DONE = 99;
+
+ private StateHelper helper;
+
+ protected StateHelper getHelper() {
+ return helper;
+ }
+
+ private boolean isPaused;
+
+ protected void setPaused(boolean tf) {
+ isPaused = tf;
+ firePropertyChange(PROPERTY_PAUSE, null, (tf ? PAUSED : RESUMED));
+ if (!tf)
+ stateLoop();
+ }
+
+ protected boolean isPaused() {
+ return isPaused;
+ }
+
+ /**
+ * The StateMachine's main loop.
+ *
+ * Note that a return from this method will exit doInBackground, trigger the
+ * isDone() state on the underying worker, and scheduling its done() for
+ * execution on the AWTEventQueue.
+ *
+ * Since this happens essentially immediately, it is unlikely that
+ * SwingWorker.isCancelled() will ever be true. Thus, the SwingWorker task
+ * itself won't be cancelable in Java or in JavaScript, since its
+ * doInBackground() method is officially complete, and isDone() is true well
+ * before we are "really" done. FutureTask will not set isCancelled() true once
+ * the task has run.
+ *
+ * We are using an asynchronous task specifically because we want to have the
+ * opportunity for the ProgressMonitor to report in JavaScript. We will have to
+ * cancel our task and report progress explicitly using our own methods.
+ *
+ */
+ @Override
+ public boolean stateLoop() {
+ while (helper.isAlive() && !isPaused) {
+ switch (helper.getState()) {
+ case STATE_INIT:
+ setProgressAsync(min);
+ initAsync();
+ helper.setState(STATE_WAIT);
+ continue;
+ case STATE_LOOP:
+ if (checkCanceled()) {
+ helper.setState(STATE_DONE);
+ firePropertyChange(PROPERTY_STATE, null, CANCELED_ASYNC);
+ } else {
+ int ret = doInBackgroundAsync(progressAsync);
+ if (!helper.isAlive() || isPaused) {
+ continue;
+ }
+ progressAsync = ret;
+ setProgressAsync(progressAsync);
+ setNote(getNote(progressAsync));
+ setProgress(progressPercent);
+ if (progressMonitor != null) {
+ progressMonitor.setProgress(max > min ? progressAsync : max + min - progressAsync);
+ }
+ helper.setState(progressAsync == max ? STATE_DONE : STATE_WAIT);
+ }
+ continue;
+ case STATE_WAIT:
+ // meaning "sleep" and then "loop"
+ helper.setState(STATE_LOOP);
+ helper.sleep(delayMillis);
+ return true;
+ default:
+ case STATE_DONE:
+ stopProgressMonitor();
+ // Put the doneAsync() method on the AWTEventQueue
+ // just as for SwingWorker.done().
+ if (isAsync) {
+ SwingUtilities.invokeLater(doneRunnable);
+ } else {
+ doneRunnable.run();
+ }
+
+ return false;
+ }
+ }
+ if (!helper.isAlive()) {
+ stopProgressMonitor();
+ }
+ return false;
+ }
+
+ private void stopProgressMonitor() {
+ if (progressMonitor != null) {
+ progressMonitor.close();
+ progressMonitor = null;
+ }
+ }
+
+ private Runnable doneRunnable = new Runnable() {
+ @Override
+ public void run() {
+ doneAsync();
+ firePropertyChange(PROPERTY_STATE, null, DONE_ASYNC);
+ }
+
+ };
+
+ private boolean checkCanceled() {
+ if (isMonitorCanceled() || isCancelled()) {
+ helper.interrupt();
+ return true;
+ }
+ return false;
+ }
+
+ //// final SwingWorker methods not to be used by subclasses ////
+
+ private boolean isMonitorCanceled() {
+ return (progressMonitor != null && progressMonitor.isCanceled());
+ }
+
+ /**
+ * see SwingWorker, made final here.
+ *
+ */
+ @Override
+ final protected Void doInBackground() throws Exception {
+ helper = new StateHelper(this);
+ setProgressAsync(min);
+ helper.next(STATE_INIT);
+ return null;
+ }
+
+ /**
+ * see SwingWorker, made final here. Nothing to do.
+ *
+ */
+ @Override
+ final public void done() {
+ }
+
+}