| Job.java |
1 /**
2 * Distribution License:
3 * JSword is free software; you can redistribute it and/or modify it under
4 * the terms of the GNU Lesser General Public License, version 2.1 or later
5 * as published by the Free Software Foundation. This program is distributed
6 * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
7 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8 * See the GNU Lesser General Public License for more details.
9 *
10 * The License is available on the internet at:
11 * http://www.gnu.org/copyleft/lgpl.html
12 * or by writing to:
13 * Free Software Foundation, Inc.
14 * 59 Temple Place - Suite 330
15 * Boston, MA 02111-1307, USA
16 *
17 * © CrossWire Bible Society, 2005 - 2016
18 *
19 */
20 package org.crosswire.common.progress;
21
22 import java.io.IOException;
23 import java.net.URI;
24 import java.util.ArrayList;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Timer;
29 import java.util.TimerTask;
30
31 import org.crosswire.common.util.NetUtil;
32 import org.crosswire.common.util.PropertyMap;
33 import org.crosswire.jsword.JSMsg;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
36
37 /**
38 * A Generic method of keeping track of Threads and monitoring their progress.
39 *
40 * @see gnu.lgpl.License The GNU Lesser General Public License for details.
41 * @author Joe Walker
42 * @author DM Smith
43 */
44 public final class Job implements Progress {
45 /**
46 * Create a new Job. This will automatically fire a workProgressed event to
47 * all WorkListeners, with the work property of this job set to 0.
48 *
49 * @param jobID the job identifier
50 * @param jobName
51 * Short description of this job
52 * @param worker
53 * Optional thread to use in request to stop worker
54 */
55 protected Job(String jobID, String jobName, Thread worker) {
56 this.jobName = jobName;
57 this.jobID = jobID;
58 this.workerThread = worker;
59 this.listeners = new ArrayList<WorkListener>();
60 this.cancelable = workerThread != null;
61 this.jobMode = ProgressMode.PREDICTIVE;
62 }
63
64 /* (non-Javadoc)
65 * @see org.crosswire.common.progress.Progress#beginJob(java.lang.String)
66 */
67 public void beginJob(String sectionName) {
68 beginJob(sectionName, 100);
69 }
70
71 /* (non-Javadoc)
72 * @see org.crosswire.common.progress.Progress#beginJob(java.lang.String, int)
73 */
74 public void beginJob(String sectionName, int totalWork) {
75 if (this.finished) {
76 return;
77 }
78
79 synchronized (this) {
80 finished = false;
81 currentSectionName = sectionName;
82 totalUnits = totalWork;
83 jobMode = totalUnits == 100 ? ProgressMode.PERCENT : ProgressMode.UNITS;
84 }
85
86 // Report that the Job has started.
87 JobManager.fireWorkProgressed(this);
88 }
89
90 /* (non-Javadoc)
91 * @see org.crosswire.common.progress.Progress#beginJob(java.lang.String, java.net.URI)
92 */
93 public void beginJob(String sectionName, URI predictURI) {
94 if (finished) {
95 return;
96 }
97
98 synchronized (this) {
99 currentSectionName = sectionName;
100 predictionMapURI = predictURI;
101 jobMode = ProgressMode.PREDICTIVE;
102 startTime = System.currentTimeMillis();
103
104 fakingTimer = new Timer();
105 fakingTimer.schedule(new PredictTask(), 0, REPORTING_INTERVAL);
106
107 // Load currentPredictionMap. It's not a disaster if it doesn't load
108 totalUnits = loadPredictions();
109
110 // There were no prior predictions so punt.
111 if (totalUnits == Progress.UNKNOWN) {
112 // if we have nothing to go on use our assumption
113 totalUnits = EXTRA_TIME;
114 jobMode = ProgressMode.UNKNOWN;
115 }
116
117 // And the predictions for next time
118 nextPredictionMap = new HashMap<String, Integer>();
119 }
120
121 // Report that the Job has started.
122 JobManager.fireWorkProgressed(this);
123 }
124
125 /* (non-Javadoc)
126 * @see org.crosswire.common.progress.Progress#getJobName()
127 */
128 public synchronized String getJobName() {
129 return jobName;
130 }
131
132 /* (non-Javadoc)
133 * @see org.crosswire.common.progress.Progress#getProgressMode()
134 */
135 public ProgressMode getProgressMode() {
136 return jobMode;
137 }
138
139 /* (non-Javadoc)
140 * @see org.crosswire.common.progress.Progress#getTotalWork()
141 */
142 public synchronized int getTotalWork() {
143 return totalUnits;
144 }
145
146 /* (non-Javadoc)
147 * @see org.crosswire.common.progress.Progress#setTotalWork(int)
148 */
149 public void setTotalWork(int totalWork) {
150 this.totalUnits = totalWork;
151 }
152
153 /* (non-Javadoc)
154 * @see org.crosswire.common.progress.Progress#getWork()
155 */
156 public int getWork() {
157 return percent;
158 }
159
160 /* (non-Javadoc)
161 * @see org.crosswire.common.progress.Progress#setWork(int)
162 */
163 public void setWork(int work) {
164 setWorkDone(work);
165 }
166
167 /* (non-Javadoc)
168 * @see org.crosswire.common.progress.Progress#getWorkDone()
169 */
170 public int getWorkDone() {
171 return workUnits;
172 }
173
174 /* (non-Javadoc)
175 * @see org.crosswire.common.progress.Progress#setWork(int)
176 */
177 public void setWorkDone(int work) {
178 if (finished) {
179 return;
180 }
181
182 synchronized (this) {
183 if (workUnits == work) {
184 return;
185 }
186
187 workUnits = work;
188
189 int oldPercent = percent;
190 percent = 100 * workUnits / totalUnits;
191 if (oldPercent == percent) {
192 return;
193 }
194 }
195
196 JobManager.fireWorkProgressed(this);
197 }
198
199 /* (non-Javadoc)
200 * @see org.crosswire.common.progress.Progress#incrementWorkDone(int)
201 */
202 public void incrementWorkDone(int step) {
203 if (finished) {
204 return;
205 }
206
207 synchronized (this) {
208 workUnits += step;
209
210 int oldPercent = percent;
211 // use long in arithmetic to avoid integer overflow
212 percent = (int) (100L * workUnits / totalUnits);
213 if (oldPercent == percent) {
214 return;
215 }
216 }
217
218 JobManager.fireWorkProgressed(this);
219 }
220
221 /* (non-Javadoc)
222 * @see org.crosswire.common.progress.Progress#getSectionName()
223 */
224 public String getSectionName() {
225 return currentSectionName;
226 }
227
228 /* (non-Javadoc)
229 * @see org.crosswire.common.progress.Progress#setSectionName(java.lang.String)
230 */
231 public void setSectionName(String sectionName) {
232 if (finished) {
233 return;
234 }
235
236 boolean doUpdate = false;
237 synchronized (this) {
238 // If we are in some kind of predictive mode, then measure progress toward the expected end.
239 if (jobMode == ProgressMode.PREDICTIVE || jobMode == ProgressMode.UNKNOWN) {
240 doUpdate = updateProgress(System.currentTimeMillis());
241
242 // We are done with the current section and are starting another
243 // So record the length of the last section
244 if (nextPredictionMap != null) {
245 nextPredictionMap.put(currentSectionName, Integer.valueOf(workUnits));
246 }
247 }
248
249 currentSectionName = sectionName;
250 }
251
252 // Don't automatically tell listeners that the label changed.
253 // Only do so if it is time to do an update.
254 if (doUpdate) {
255 JobManager.fireWorkProgressed(this);
256 }
257 }
258
259 /* (non-Javadoc)
260 * @see org.crosswire.common.progress.Progress#done()
261 */
262 public void done() {
263 // TRANSLATOR: This shows up in a progress bar when progress is finished.
264 String sectionName = JSMsg.gettext("Done");
265
266 synchronized (this) {
267 finished = true;
268
269 currentSectionName = sectionName;
270
271 // Turn off the timer
272 if (fakingTimer != null) {
273 fakingTimer.cancel();
274 fakingTimer = null;
275 }
276
277 workUnits = totalUnits;
278 percent = 100;
279
280 if (nextPredictionMap != null) {
281 nextPredictionMap.put(currentSectionName, Integer.valueOf((int) (System.currentTimeMillis() - startTime)));
282 }
283 }
284
285 // Report that the job is done.
286 JobManager.fireWorkProgressed(this);
287
288 synchronized (this) {
289 if (predictionMapURI != null) {
290 savePredictions();
291 }
292 }
293 }
294
295 /* (non-Javadoc)
296 * @see org.crosswire.common.progress.Progress#cancel()
297 */
298 public void cancel() {
299 if (!finished) {
300 ignoreTimings();
301 done();
302 if (workerThread != null) {
303 workerThread.interrupt();
304 }
305 }
306 }
307
308 /* (non-Javadoc)
309 * @see org.crosswire.common.progress.Progress#isFinished()
310 */
311 public boolean isFinished() {
312 return finished;
313 }
314
315 /* (non-Javadoc)
316 * @see org.crosswire.common.progress.Progress#isCancelable()
317 */
318 public boolean isCancelable() {
319 return cancelable;
320 }
321
322 /* (non-Javadoc)
323 * @see org.crosswire.common.progress.Progress#setCancelable(boolean)
324 */
325 public void setCancelable(boolean newInterruptable) {
326 if (workerThread == null || finished) {
327 return;
328 }
329 cancelable = newInterruptable;
330 fireStateChanged();
331 }
332
333 /**
334 * Add a listener to the list
335 *
336 * @param li the interested listener
337 */
338 public synchronized void addWorkListener(WorkListener li) {
339 List<WorkListener> temp = new ArrayList<WorkListener>();
340 temp.addAll(listeners);
341
342 if (!temp.contains(li)) {
343 temp.add(li);
344 listeners = temp;
345 }
346 }
347
348 /**
349 * Remote a listener from the list
350 *
351 * @param li the disinterested listener
352 */
353 public synchronized void removeWorkListener(WorkListener li) {
354 if (listeners.contains(li)) {
355 List<WorkListener> temp = new ArrayList<WorkListener>();
356 temp.addAll(listeners);
357 temp.remove(li);
358 listeners = temp;
359 }
360 }
361
362 protected void fireStateChanged() {
363 final WorkEvent ev = new WorkEvent(this);
364
365 // we need to keep the synchronized section very small to avoid deadlock
366 // certainly keep the event dispatch clear of the synchronized block or
367 // there will be a deadlock
368 final List<WorkListener> temp = new ArrayList<WorkListener>();
369 synchronized (this) {
370 if (listeners != null) {
371 temp.addAll(listeners);
372 }
373 }
374
375 // We ought only to tell listeners about jobs that are in our
376 // list of jobs so we need to fire before delete.
377 int count = temp.size();
378 for (int i = 0; i < count; i++) {
379 temp.get(i).workStateChanged(ev);
380 }
381 }
382
383 /**
384 * Get estimated the percent progress
385 *
386 * @param now the current point in progress
387 * @return true if there is an update to progress.
388 */
389 protected synchronized boolean updateProgress(long now) {
390 int oldPercent = percent;
391 workUnits = (int) (now - startTime);
392
393 // Are we taking more time than expected?
394 // Then we are at 100%
395 if (workUnits > totalUnits) {
396 workUnits = totalUnits;
397 percent = 100;
398 } else {
399 percent = 100 * workUnits / totalUnits;
400 }
401 return oldPercent != percent;
402 }
403
404 /**
405 * Load the predictive timings if any
406 *
407 * @return the length of progress
408 */
409 private int loadPredictions() {
410 int maxAge = UNKNOWN;
411 try {
412 currentPredictionMap = new HashMap<String, Integer>();
413 PropertyMap temp = NetUtil.loadProperties(predictionMapURI);
414
415 // Determine the predicted time from the current prediction map
416 for (String title : temp.keySet()) {
417 String timestr = temp.get(title);
418
419 try {
420 Integer time = Integer.valueOf(timestr);
421 currentPredictionMap.put(title, time);
422
423 // if this time is later than the latest
424 int age = time.intValue();
425 if (maxAge < age) {
426 maxAge = age;
427 }
428 } catch (NumberFormatException ex) {
429 log.error("Time format error", ex);
430 }
431 }
432 } catch (IOException ex) {
433 log.debug("Failed to load prediction times - guessing");
434 }
435
436 return maxAge;
437 }
438
439 /**
440 * Save the known timings to a properties file.
441 */
442 private void savePredictions() {
443 // Now we know the start and the end we can convert all times to
444 // percents
445 PropertyMap predictions = new PropertyMap();
446 for (String sectionName : nextPredictionMap.keySet()) {
447 Integer age = nextPredictionMap.get(sectionName);
448 predictions.put(sectionName, age.toString());
449 }
450
451 // And save. It's not a disaster if this goes wrong
452 try {
453 NetUtil.storeProperties(predictions, predictionMapURI, "Predicted Startup Times");
454 } catch (IOException ex) {
455 log.error("Failed to save prediction times", ex);
456 }
457 }
458
459 /**
460 * Typically called from in a catch block, this ensures that we don't save
461 * the timing file because we have a messed up run.
462 */
463 private synchronized void ignoreTimings() {
464 predictionMapURI = null;
465 }
466
467 private static final int REPORTING_INTERVAL = 100;
468
469 /**
470 * The amount of extra time if the predicted time was off and more time is needed.
471 */
472 private static final int EXTRA_TIME = 2 * REPORTING_INTERVAL;
473
474 /**
475 * The type of job being performed. This is used to simplify code.
476 */
477 private ProgressMode jobMode;
478
479 /**
480 * Total amount of work to do.
481 */
482 private int totalUnits;
483
484 /**
485 * Does this job allow interruptions?
486 */
487 private boolean cancelable;
488
489 /**
490 * Have we just finished?
491 */
492 private boolean finished;
493
494 /**
495 * The amount of work done against the total.
496 */
497 private int workUnits;
498
499 /**
500 * The officially reported progress
501 */
502 private int percent;
503
504 /**
505 * A short descriptive phrase
506 */
507 private String jobName;
508
509 private final String jobID;
510 /**
511 * Optional thread to monitor progress
512 */
513 private Thread workerThread;
514
515 /**
516 * Description of what we are doing
517 */
518 private String currentSectionName;
519
520 /**
521 * The URI to which we load and save timings
522 */
523 private URI predictionMapURI;
524
525 /**
526 * The timings loaded from where they were saved after the last run
527 */
528 private Map<String, Integer> currentPredictionMap;
529
530 /**
531 * The timings as measured this time
532 */
533 private Map<String, Integer> nextPredictionMap;
534
535 /**
536 * When did this job start? Measured in milliseconds since beginning of epoch.
537 */
538 private long startTime;
539
540 /**
541 * The timer that lets us post fake progress events.
542 */
543 private Timer fakingTimer;
544
545 /**
546 * People that want to know about "cancelable" changes
547 */
548 private List<WorkListener> listeners;
549
550 /**
551 * The Job ID associated with this job
552 * @return the job ID
553 */
554 public String getJobID() {
555 return jobID;
556 }
557
558 /**
559 * So we can fake progress for Jobs that don't tell us how they are doing
560 */
561 final class PredictTask extends TimerTask {
562 /* (non-Javadoc)
563 * @see java.util.TimerTask#run()
564 */
565 @Override
566 public void run() {
567 if (updateProgress(System.currentTimeMillis())) {
568 JobManager.fireWorkProgressed(Job.this);
569 }
570 }
571 }
572
573 /**
574 * The log stream
575 */
576 private static final Logger log = LoggerFactory.getLogger(Job.class);
577 }
578