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