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