JAL-3446 AsyncSwingWorker upgrade
[jalview.git] / src / javajs / async / AsyncSwingWorker.java
1 package javajs.async;
2
3 import java.awt.Component;
4
5 import javax.swing.ProgressMonitor;
6 import javax.swing.SwingUtilities;
7 import javax.swing.SwingWorker;
8
9 import javajs.async.SwingJSUtils.StateHelper;
10 import javajs.async.SwingJSUtils.StateMachine;
11
12 /**
13  * v. 2020.06.03 
14  * 
15  * Executes synchronous or asynchronous tasks using a SwingWorker in Java or
16  * JavaScript, equivalently.
17  * 
18  * Unlike a standard SwingWorker, AsyncSwingWorker may itself be asynchronous.
19  * For example, it might load a file asynchronously, or carry out a background
20  * process in JavaScript much like one might be done in Java, but with only a
21  * single thread.
22  * 
23  * Whereas a standard SwingWorker would execute done() long before the
24  * asynchronous task completed, this class will wait until progress has been
25  * asynchronously set greater or equal to its max value or the task is canceled
26  * before executing that method.
27  * 
28  * Three methods must be supplied by the subclass:
29  * 
30  * void initAsync()
31  * 
32  * int doInBackgroundAsync(int progress)
33  * 
34  * void doneAsync()
35  * 
36  * Both initAsync() and doneAsync() are technically optional - they may be
37  * empty. doInBackgroundAsync(), however, is the key method where, like
38  * SwingWorker's doInBackground, the main work is done. The supplied progress
39  * parameter reminds the subclass of where it is at, and the return value allows
40  * the subclass to update the progress field in both the SwingWorker and the
41  * ProgressMonitor.
42  * 
43  * If it is desired to run the AsyncSwingWorker synchronously, call the
44  * executeSynchronously() method rather than execute(). Never call
45  * SwingWorker.run().
46  * 
47  * Note that doInBackgroundAsync runs on the Java AWT event queue. This means
48  * that, unlike a true SwingWorker, it will run in event-queue sequence, after
49  * anything that that method itself adds to the queue. This is what SwingWorker itself
50  * does with its done() signal. 
51  * 
52  * If doInBackgroundAsync has tasks that are time intensive, the thing to do is to
53  * 
54  * (a) pause this worker by setting the value of progress for the NEXT step:
55  * 
56  *    setProgressAsync(n);
57  *    
58  * (b) pause the timer so that when doInBackgroundAsync returns, the timer is not fired:
59  * 
60  *    setPaused(true);
61  * 
62  * (c) start your process as new Thread, which bypasses the AWT EventQueue:
63  * 
64  *    new Thread(Runnable).start();
65  *    
66  * (d) have your thread, when it is done, return control to this worker:
67  * 
68  *    setPaused(false);
69  *    
70  * This final call restarts the worker with the currently specified progress value.
71  * 
72  * @author hansonr
73  *
74  */
75 public abstract class AsyncSwingWorker extends SwingWorker<Void, Void> implements StateMachine {
76
77
78         // PropertyChangeEvent getPropertyName()
79         
80         private static final String PROPERTY_STATE = "state";
81         private static final String PROPERTY_PAUSE = "pause";
82         
83         // PropertyChangeEvent getNewValue()
84         
85         public static final String STARTED_ASYNC = "STARTED_ASYNC";
86         public static final String STARTED_SYNC = "STARTED_SYNC";
87         
88         public static final String DONE_ASYNC = "DONE_ASYNC";
89         public static final String CANCELED_ASYNC = "CANCELED_ASYNC";
90         
91         public static final String PAUSED = "PAUSED";
92         public static final String RESUMED = "RESUMED";
93
94         protected int progressAsync;
95
96         /**
97          * Override to provide initial tasks.
98          */
99         abstract public void initAsync();
100
101         /**
102          * Given the last progress, do some portion of the task that the SwingWorker
103          * would do in the background, and return the new progress. returning max or
104          * above will complete the task.
105          * 
106          * @param progress
107          * @return new progress
108          */
109         abstract public int doInBackgroundAsync(int progress);
110
111         /**
112          * Do something when the task is finished or canceled.
113          * 
114          */
115         abstract public void doneAsync();
116
117         protected ProgressMonitor progressMonitor;
118
119         protected int delayMillis;
120         protected String note;
121         protected int min;
122         protected int max;
123         protected int progressPercent;
124
125         protected boolean isAsync;
126         private Exception exception;
127
128         /**
129          * Construct an asynchronous SwingWorker task that optionally will display a
130          * ProgressMonitor. Progress also can be monitored by adding a
131          * PropertyChangeListener to the AsyncSwingWorker and looking for the "progress"
132          * event, just the same as for a standard SwingWorker.
133          * 
134          * @param owner       optional owner for the ProgressMonitor, typically a JFrame
135          *                    or JDialog.
136          * 
137          * @param title       A non-null title indicates we want to use a
138          *                    ProgressMonitor with that title line.
139          * 
140          * @param delayMillis A positive number indicating the delay we want before
141          *                    executions, during which progress will be reported.
142          * 
143          * @param min         The first progress value. No range limit.
144          * 
145          * @param max         The last progress value. No range limit; may be greater
146          *                    than min.
147          * 
148          */
149         public AsyncSwingWorker(Component owner, String title, int delayMillis, int min, int max) {
150                 if (title != null && delayMillis > 0) {
151                         progressMonitor = new ProgressMonitor(owner, title, "", Math.min(min, max), Math.max(min, max));
152                         progressMonitor.setProgress(Math.min(min, max)); // displays monitor
153                 }
154                 this.delayMillis = Math.max(0, delayMillis);
155                 this.isAsync = (delayMillis > 0);
156
157                 this.min = min;
158                 this.max = max;
159         }
160
161         public void executeAsync() {
162                 firePropertyChange(PROPERTY_STATE, null, STARTED_ASYNC);
163                 super.execute();
164         }
165
166         public void executeSynchronously() {
167                 firePropertyChange(PROPERTY_STATE, null, STARTED_SYNC);
168                 isAsync = false;
169                 delayMillis = 0;
170                 try {
171                         doInBackground();
172                 } catch (Exception e) {
173                         exception = e;
174                         e.printStackTrace();
175                         cancelAsync();
176                 }
177         }
178
179         public Exception getException() {
180                 return exception;
181         }
182
183         public int getMinimum() {
184                 return min;
185         }
186
187         public void setMinimum(int min) {
188                 this.min = min;
189                 if (progressMonitor != null) {
190                         progressMonitor.setMinimum(min);
191                 }
192         }
193
194         public int getMaximum() {
195                 return max;
196         }
197
198         public void setMaximum(int max) {
199                 if (progressMonitor != null) {
200                         progressMonitor.setMaximum(max);
201                 }
202                 this.max = max;
203         }
204
205         public int getProgressPercent() {
206                 return progressPercent;
207         }
208
209         public void setNote(String note) {
210                 this.note = note;
211                 if (progressMonitor != null) {
212                         progressMonitor.setNote(note);
213                 }
214         }
215
216         /**
217          * Cancel the asynchronous process.
218          * 
219          */
220         public void cancelAsync() {
221                 helper.interrupt();
222         }
223
224         /**
225          * Check to see if the asynchronous process has been canceled.
226          *
227          * @return true if StateHelper is not alive anymore
228          * 
229          */
230         public boolean isCanceledAsync() {
231                 return !helper.isAlive();
232         }
233
234         /**
235          * Check to see if the asynchronous process is completely done.
236          * 
237          * @return true only if the StateMachine is at STATE_DONE
238          * 
239          */
240         public boolean isDoneAsync() {
241                 return helper.getState() == STATE_DONE;
242         }
243
244         /**
245          * Override to set a more informed note for the ProcessMonitor.
246          * 
247          * @param progress
248          * @return
249          */
250         public String getNote(int progress) {
251                 return String.format("Completed %d%%.\n", progress);
252         }
253
254         /**
255          * Retrieve the last note delivered by the ProcessMonitor.
256          * 
257          * @return
258          */
259         public String getNote() {
260                 return note;
261         }
262
263         public int getProgressAsync() {
264                 return progressAsync;
265         }
266
267         /**
268          * Set the [min,max] progress safely.
269          * 
270          * SwingWorker only allows progress between 0 and 100. This method safely
271          * translates [min,max] to [0,100].
272          * 
273          * @param n
274          */
275         public void setProgressAsync(int n) {
276                 n = (max > min ? Math.max(min, Math.min(n, max)) : Math.max(max, Math.min(n, min)));
277                 progressAsync = n;
278                 n = (n - min) * 100 / (max - min);
279                 n = (n < 0 ? 0 : n > 100 ? 100 : n);
280                 progressPercent = n;
281         }
282
283         ///// the StateMachine /////
284
285         private final static int STATE_INIT = 0;
286         private final static int STATE_LOOP = 1;
287         private final static int STATE_WAIT = 2;
288         private final static int STATE_DONE = 99;
289         
290         private StateHelper helper;
291
292         protected StateHelper getHelper() {
293                 return helper;
294         }
295
296         private boolean isPaused;
297
298         protected void setPaused(boolean tf) {
299                 isPaused = tf;
300                 firePropertyChange(PROPERTY_PAUSE, null, (tf ? PAUSED : RESUMED));
301                 if (!tf)
302                         stateLoop();
303         }
304
305         protected boolean isPaused() {
306                 return isPaused;
307         }
308
309         /**
310          * The StateMachine's main loop.
311          * 
312          * Note that a return from this method will exit doInBackground, trigger the
313          * isDone() state on the underying worker, and scheduling its done() for
314          * execution on the AWTEventQueue.
315          *
316          * Since this happens essentially immediately, it is unlikely that
317          * SwingWorker.isCancelled() will ever be true. Thus, the SwingWorker task
318          * itself won't be cancelable in Java or in JavaScript, since its
319          * doInBackground() method is officially complete, and isDone() is true well
320          * before we are "really" done. FutureTask will not set isCancelled() true once
321          * the task has run.
322          * 
323          * We are using an asynchronous task specifically because we want to have the
324          * opportunity for the ProgressMonitor to report in JavaScript. We will have to
325          * cancel our task and report progress explicitly using our own methods.
326          * 
327          */
328         @Override
329         public boolean stateLoop() {
330                 while (helper.isAlive() && !isPaused) {
331                         switch (helper.getState()) {
332                         case STATE_INIT:
333                                 setProgressAsync(min);
334                                 initAsync();
335                                 helper.setState(STATE_WAIT);
336                                 continue;
337                         case STATE_LOOP:
338                                 if (checkCanceled()) {
339                                         helper.setState(STATE_DONE);
340                                         firePropertyChange(PROPERTY_STATE, null, CANCELED_ASYNC);
341                                 } else {
342                                         int ret = doInBackgroundAsync(progressAsync);                                   
343                                         if (!helper.isAlive() || isPaused) {
344                                                 continue;
345                                         }
346                                         progressAsync = ret;
347                                         setProgressAsync(progressAsync);
348                                         setNote(getNote(progressAsync));
349                                         setProgress(progressPercent);
350                                         if (progressMonitor != null) {
351                                                 progressMonitor.setProgress(max > min ? progressAsync : max + min - progressAsync);
352                                         }
353                                         helper.setState(progressAsync == max ? STATE_DONE : STATE_WAIT);
354                                 }
355                                 continue;
356                         case STATE_WAIT:
357                                 // meaning "sleep" and then "loop"
358                                 helper.setState(STATE_LOOP);
359                                 helper.sleep(delayMillis);
360                                 return true;
361                         default:
362                         case STATE_DONE:
363                                 stopProgressMonitor();
364                                 // Put the doneAsync() method on the AWTEventQueue
365                                 // just as for SwingWorker.done().
366                                 if (isAsync) {
367                                         SwingUtilities.invokeLater(doneRunnable);
368                                 } else {
369                                         doneRunnable.run();
370                                 }
371
372                                 return false;
373                         }
374                 }
375                 if (!helper.isAlive()) {
376                         stopProgressMonitor();
377                 }
378                 return false;
379         }
380
381         private void stopProgressMonitor() {
382                 if (progressMonitor != null) {
383                         progressMonitor.close();
384                         progressMonitor = null;
385                 }
386         }
387
388         private Runnable doneRunnable = new Runnable() {
389                 @Override
390                 public void run() {
391                         doneAsync();
392                         firePropertyChange(PROPERTY_STATE, null, DONE_ASYNC);
393                 }
394
395         };
396
397         private boolean checkCanceled() {
398                 if (isMonitorCanceled() || isCancelled()) {
399                         helper.interrupt();
400                         return true;
401                 }
402                 return false;
403         }
404
405         //// final SwingWorker methods not to be used by subclasses ////
406
407         private boolean isMonitorCanceled() {
408                 return (progressMonitor != null && progressMonitor.isCanceled());
409         }
410
411         /**
412          * see SwingWorker, made final here.
413          * 
414          */
415         @Override
416         final protected Void doInBackground() throws Exception {
417                 helper = new StateHelper(this);
418                 setProgressAsync(min);
419                 helper.next(STATE_INIT);
420                 return null;
421         }
422
423         /**
424          * see SwingWorker, made final here. Nothing to do.
425          * 
426          */
427         @Override
428         final public void done() {
429         }
430
431 }