| 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 as published by
5 * the Free Software Foundation. This program is distributed in the hope
6 * that it will be useful, but WITHOUT ANY WARRANTY; without even the
7 * 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 * Copyright: 2005
18 * The copyright to this program is held by it's authors.
19 *
20 * ID: $Id: Job.java 2235 2012-03-07 19:23:21Z mjdenham $
21 */
22 package org.crosswire.common.progress;
23
24 import java.io.IOException;
25 import java.net.URI;
26 import java.util.ArrayList;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Timer;
31 import java.util.TimerTask;
32
33 import org.crosswire.common.util.Logger;
34 import org.crosswire.common.util.NetUtil;
35 import org.crosswire.common.util.PropertyMap;
36 import org.crosswire.jsword.JSMsg;
37
38 /**
39 * A Generic method of keeping track of Threads and monitoring their progress.
40 *
41 * @see gnu.lgpl.License for license details.<br>
42 * The copyright to this program is held by it's authors.
43 * @author Joe Walker [joe at eireneh dot com]
44 * @author DM Smith [dmsmith555 at yahoo dot com]
45 */
46 public final class Job implements Progress {
47 /**
48 * Create a new Job. This will automatically fire a workProgressed event to
49 * all WorkListeners, with the work property of this job set to 0.
50 *
51 * @param description
52 * Short description of this job
53 * @param worker
54 * Optional thread to use in request to stop worker
55 */
56 protected Job(String jobName, Thread worker) {
57 this.jobName = jobName;
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 synchronized 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 synchronized void setTotalWork(int totalWork) {
150 this.totalUnits = totalWork;
151 }
152
153 /* (non-Javadoc)
154 * @see org.crosswire.common.progress.Progress#getWork()
155 */
156 public synchronized int getWork() {
157 return percent;
158 }
159
160 /* (non-Javadoc)
161 * @see org.crosswire.common.progress.Progress#setWork(int)
162 */
163 public synchronized void setWork(int work) {
164 setWorkDone(work);
165 }
166
167 /* (non-Javadoc)
168 * @see org.crosswire.common.progress.Progress#getWorkDone()
169 */
170 public synchronized 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 synchronized 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 synchronized 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 public synchronized void addWorkListener(WorkListener li) {
337 List<WorkListener> temp = new ArrayList<WorkListener>();
338 temp.addAll(listeners);
339
340 if (!temp.contains(li)) {
341 temp.add(li);
342 listeners = temp;
343 }
344 }
345
346 /**
347 * Remote a listener from the list
348 */
349 public synchronized void removeWorkListener(WorkListener li) {
350 if (listeners.contains(li)) {
351 List<WorkListener> temp = new ArrayList<WorkListener>();
352 temp.addAll(listeners);
353 temp.remove(li);
354 listeners = temp;
355 }
356 }
357
358 protected void fireStateChanged() {
359 final WorkEvent ev = new WorkEvent(this);
360
361 // we need to keep the synchronized section very small to avoid deadlock
362 // certainly keep the event dispatch clear of the synchronized block or
363 // there will be a deadlock
364 final List<WorkListener> temp = new ArrayList<WorkListener>();
365 synchronized (this) {
366 if (listeners != null) {
367 temp.addAll(listeners);
368 }
369 }
370
371 // We ought only to tell listeners about jobs that are in our
372 // list of jobs so we need to fire before delete.
373 int count = temp.size();
374 for (int i = 0; i < count; i++) {
375 temp.get(i).workStateChanged(ev);
376 }
377 }
378
379 /**
380 * Get estimated the percent progress
381 *
382 * @return true if there is an update to progress.
383 */
384 protected synchronized boolean updateProgress(long now) {
385 int oldPercent = percent;
386 workUnits = (int) (now - startTime);
387
388 // Are we taking more time than expected?
389 // Then we are at 100%
390 if (workUnits > totalUnits) {
391 workUnits = totalUnits;
392 percent = 100;
393 } else {
394 percent = 100 * workUnits / totalUnits;
395 }
396 return oldPercent != percent;
397 }
398
399 /**
400 * Load the predictive timings if any
401 */
402 private synchronized int loadPredictions() {
403 int maxAge = UNKNOWN;
404 try {
405 currentPredictionMap = new HashMap<String, Integer>();
406 PropertyMap temp = NetUtil.loadProperties(predictionMapURI);
407
408 // Determine the predicted time from the current prediction map
409 for (String title : temp.keySet()) {
410 String timestr = temp.get(title);
411
412 try {
413 Integer time = Integer.valueOf(timestr);
414 currentPredictionMap.put(title, time);
415
416 // if this time is later than the latest
417 int age = time.intValue();
418 if (maxAge < age) {
419 maxAge = age;
420 }
421 } catch (NumberFormatException ex) {
422 log.error("Time format error", ex);
423 }
424 }
425 } catch (IOException ex) {
426 log.debug("Failed to load prediction times - guessing");
427 }
428
429 return maxAge;
430 }
431
432 /**
433 * Save the known timings to a properties file.
434 */
435 private synchronized void savePredictions() {
436 // Now we know the start and the end we can convert all times to
437 // percents
438 PropertyMap predictions = new PropertyMap();
439 for (String sectionName : nextPredictionMap.keySet()) {
440 Integer age = nextPredictionMap.get(sectionName);
441 predictions.put(sectionName, age.toString());
442 }
443
444 // And save. It's not a disaster if this goes wrong
445 try {
446 NetUtil.storeProperties(predictions, predictionMapURI, "Predicted Startup Times");
447 } catch (IOException ex) {
448 log.error("Failed to save prediction times", ex);
449 }
450 }
451
452 /**
453 * Typically called from in a catch block, this ensures that we don't save
454 * the timing file because we have a messed up run.
455 */
456 private synchronized void ignoreTimings() {
457 predictionMapURI = null;
458 }
459
460 private static final int REPORTING_INTERVAL = 100;
461
462 /**
463 * The amount of extra time if the predicted time was off and more time is needed.
464 */
465 private static final int EXTRA_TIME = 2 * REPORTING_INTERVAL;
466
467 /**
468 * The type of job being performed. This is used to simplify code.
469 */
470 private ProgressMode jobMode;
471
472 /**
473 * Total amount of work to do.
474 */
475 private int totalUnits;
476
477 /**
478 * Does this job allow interruptions?
479 */
480 private boolean cancelable;
481
482 /**
483 * Have we just finished?
484 */
485 private boolean finished;
486
487 /**
488 * The amount of work done against the total.
489 */
490 private int workUnits;
491
492 /**
493 * The officially reported progress
494 */
495 private int percent;
496
497 /**
498 * A short descriptive phrase
499 */
500 private String jobName;
501
502 /**
503 * Optional thread to monitor progress
504 */
505 private Thread workerThread;
506
507 /**
508 * Description of what we are doing
509 */
510 private String currentSectionName;
511
512 /**
513 * The URI to which we load and save timings
514 */
515 private URI predictionMapURI;
516
517 /**
518 * The timings loaded from where they were saved after the last run
519 */
520 private Map<String, Integer> currentPredictionMap;
521
522 /**
523 * The timings as measured this time
524 */
525 private Map<String, Integer> nextPredictionMap;
526
527 /**
528 * When did this job start? Measured in milliseconds since beginning of epoch.
529 */
530 private long startTime;
531
532 /**
533 * The timer that lets us post fake progress events.
534 */
535 private Timer fakingTimer;
536
537 /**
538 * People that want to know about "cancelable" changes
539 */
540 private List<WorkListener> listeners;
541
542 /**
543 * So we can fake progress for Jobs that don't tell us how they are doing
544 */
545 final class PredictTask extends TimerTask {
546 /* (non-Javadoc)
547 * @see java.util.TimerTask#run()
548 */
549 @Override
550 public void run() {
551 if (updateProgress(System.currentTimeMillis())) {
552 JobManager.fireWorkProgressed(Job.this);
553 }
554 }
555 }
556
557 /**
558 * The log stream
559 */
560 private static final Logger log = Logger.getLogger(Job.class);
561 }
562