Coverage Report - org.crosswire.common.progress.Job
 
Classes in this File Line Coverage Branch Coverage Complexity
Job
0%
0/176
0%
0/62
2.448
Job$PredictTask
0%
0/4
0%
0/2
2.448
 
 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  0
     protected Job(String jobID, String jobName, Thread worker) {
 56  0
         this.jobName = jobName;
 57  0
         this.jobID = jobID;
 58  0
         this.workerThread = worker;
 59  0
         this.listeners = new ArrayList<WorkListener>();
 60  0
         this.cancelable = workerThread != null;
 61  0
         this.jobMode = ProgressMode.PREDICTIVE;
 62  0
     }
 63  
 
 64  
     /* (non-Javadoc)
 65  
      * @see org.crosswire.common.progress.Progress#beginJob(java.lang.String)
 66  
      */
 67  
     public void beginJob(String sectionName) {
 68  0
         beginJob(sectionName, 100);
 69  0
     }
 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  0
         if (this.finished) {
 76  0
             return;
 77  
         }
 78  
 
 79  0
         synchronized (this) {
 80  0
             finished = false;
 81  0
             currentSectionName = sectionName;
 82  0
             totalUnits = totalWork;
 83  0
             jobMode = totalUnits == 100 ? ProgressMode.PERCENT : ProgressMode.UNITS;
 84  0
         }
 85  
 
 86  
         // Report that the Job has started.
 87  0
         JobManager.fireWorkProgressed(this);
 88  0
     }
 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  0
         if (finished) {
 95  0
             return;
 96  
         }
 97  
 
 98  0
         synchronized (this) {
 99  0
             currentSectionName = sectionName;
 100  0
             predictionMapURI = predictURI;
 101  0
             jobMode = ProgressMode.PREDICTIVE;
 102  0
             startTime = System.currentTimeMillis();
 103  
 
 104  0
             fakingTimer = new Timer();
 105  0
             fakingTimer.schedule(new PredictTask(), 0, REPORTING_INTERVAL);
 106  
 
 107  
             // Load currentPredictionMap. It's not a disaster if it doesn't load
 108  0
             totalUnits = loadPredictions();
 109  
 
 110  
             // There were no prior predictions so punt.
 111  0
             if (totalUnits == Progress.UNKNOWN) {
 112  
                 // if we have nothing to go on use our assumption
 113  0
                 totalUnits = EXTRA_TIME;
 114  0
                 jobMode = ProgressMode.UNKNOWN;
 115  
             }
 116  
 
 117  
             // And the predictions for next time
 118  0
             nextPredictionMap = new HashMap<String, Integer>();
 119  0
         }
 120  
 
 121  
         // Report that the Job has started.
 122  0
         JobManager.fireWorkProgressed(this);
 123  0
     }
 124  
 
 125  
     /* (non-Javadoc)
 126  
      * @see org.crosswire.common.progress.Progress#getJobName()
 127  
      */
 128  
     public synchronized String getJobName() {
 129  0
         return jobName;
 130  
     }
 131  
 
 132  
     /* (non-Javadoc)
 133  
      * @see org.crosswire.common.progress.Progress#getProgressMode()
 134  
      */
 135  
     public ProgressMode getProgressMode() {
 136  0
         return jobMode;
 137  
     }
 138  
 
 139  
     /* (non-Javadoc)
 140  
      * @see org.crosswire.common.progress.Progress#getTotalWork()
 141  
      */
 142  
     public synchronized int getTotalWork() {
 143  0
         return totalUnits;
 144  
     }
 145  
 
 146  
     /* (non-Javadoc)
 147  
      * @see org.crosswire.common.progress.Progress#setTotalWork(int)
 148  
      */
 149  
     public void setTotalWork(int totalWork) {
 150  0
         this.totalUnits = totalWork;
 151  0
     }
 152  
 
 153  
     /* (non-Javadoc)
 154  
      * @see org.crosswire.common.progress.Progress#getWork()
 155  
      */
 156  
     public int getWork() {
 157  0
         return percent;
 158  
     }
 159  
 
 160  
     /* (non-Javadoc)
 161  
      * @see org.crosswire.common.progress.Progress#setWork(int)
 162  
      */
 163  
     public void setWork(int work) {
 164  0
         setWorkDone(work);
 165  0
     }
 166  
 
 167  
     /* (non-Javadoc)
 168  
      * @see org.crosswire.common.progress.Progress#getWorkDone()
 169  
      */
 170  
     public int getWorkDone() {
 171  0
         return workUnits;
 172  
     }
 173  
 
 174  
     /* (non-Javadoc)
 175  
      * @see org.crosswire.common.progress.Progress#setWork(int)
 176  
      */
 177  
     public void setWorkDone(int work) {
 178  0
         if (finished) {
 179  0
             return;
 180  
         }
 181  
 
 182  0
         synchronized (this) {
 183  0
             if (workUnits == work) {
 184  0
                 return;
 185  
             }
 186  
 
 187  0
             workUnits = work;
 188  
 
 189  0
             int oldPercent = percent;
 190  0
             percent = 100 * workUnits / totalUnits;
 191  0
             if (oldPercent == percent) {
 192  0
                 return;
 193  
             }
 194  0
         }
 195  
 
 196  0
         JobManager.fireWorkProgressed(this);
 197  0
     }
 198  
 
 199  
     /* (non-Javadoc)
 200  
      * @see org.crosswire.common.progress.Progress#incrementWorkDone(int)
 201  
      */
 202  
     public void incrementWorkDone(int step) {
 203  0
         if (finished) {
 204  0
             return;
 205  
         }
 206  
 
 207  0
         synchronized (this) {
 208  0
             workUnits += step;
 209  
 
 210  0
             int oldPercent = percent;
 211  
             // use long in arithmetic to avoid integer overflow 
 212  0
             percent = (int) (100L * workUnits / totalUnits);
 213  0
             if (oldPercent == percent) {
 214  0
                 return;
 215  
             }
 216  0
         }
 217  
 
 218  0
         JobManager.fireWorkProgressed(this);
 219  0
     }
 220  
 
 221  
     /* (non-Javadoc)
 222  
      * @see org.crosswire.common.progress.Progress#getSectionName()
 223  
      */
 224  
     public String getSectionName() {
 225  0
         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  0
         if (finished) {
 233  0
             return;
 234  
         }
 235  
 
 236  0
         boolean doUpdate = false;
 237  0
         synchronized (this) {
 238  
             // If we are in some kind of predictive mode, then measure progress toward the expected end.
 239  0
             if (jobMode == ProgressMode.PREDICTIVE || jobMode == ProgressMode.UNKNOWN) {
 240  0
                 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  0
                 if (nextPredictionMap != null) {
 245  0
                     nextPredictionMap.put(currentSectionName, Integer.valueOf(workUnits));
 246  
                 }
 247  
             }
 248  
 
 249  0
             currentSectionName = sectionName;
 250  0
         }
 251  
 
 252  
         // Don't automatically tell listeners that the label changed.
 253  
         // Only do so if it is time to do an update.
 254  0
         if (doUpdate) {
 255  0
             JobManager.fireWorkProgressed(this);
 256  
         }
 257  0
     }
 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  0
         String sectionName = JSMsg.gettext("Done");
 265  
 
 266  0
         synchronized (this) {
 267  0
             finished = true;
 268  
 
 269  0
             currentSectionName = sectionName;
 270  
 
 271  
             // Turn off the timer
 272  0
             if (fakingTimer != null) {
 273  0
                 fakingTimer.cancel();
 274  0
                 fakingTimer = null;
 275  
             }
 276  
 
 277  0
             workUnits = totalUnits;
 278  0
             percent = 100;
 279  
 
 280  0
             if (nextPredictionMap != null) {
 281  0
                 nextPredictionMap.put(currentSectionName, Integer.valueOf((int) (System.currentTimeMillis() - startTime)));
 282  
             }
 283  0
         }
 284  
 
 285  
         // Report that the job is done.
 286  0
         JobManager.fireWorkProgressed(this);
 287  
 
 288  0
         synchronized (this) {
 289  0
             if (predictionMapURI != null) {
 290  0
                 savePredictions();
 291  
             }
 292  0
         }
 293  0
     }
 294  
 
 295  
     /* (non-Javadoc)
 296  
      * @see org.crosswire.common.progress.Progress#cancel()
 297  
      */
 298  
     public void cancel() {
 299  0
         if (!finished) {
 300  0
             ignoreTimings();
 301  0
             done();
 302  0
             if (workerThread != null) {
 303  0
                 workerThread.interrupt();
 304  
             }
 305  
         }
 306  0
     }
 307  
 
 308  
    /* (non-Javadoc)
 309  
      * @see org.crosswire.common.progress.Progress#isFinished()
 310  
      */
 311  
     public boolean isFinished() {
 312  0
         return finished;
 313  
     }
 314  
 
 315  
     /* (non-Javadoc)
 316  
      * @see org.crosswire.common.progress.Progress#isCancelable()
 317  
      */
 318  
     public boolean isCancelable() {
 319  0
         return cancelable;
 320  
     }
 321  
 
 322  
     /* (non-Javadoc)
 323  
      * @see org.crosswire.common.progress.Progress#setCancelable(boolean)
 324  
      */
 325  
     public void setCancelable(boolean newInterruptable) {
 326  0
         if (workerThread == null || finished) {
 327  0
             return;
 328  
         }
 329  0
         cancelable = newInterruptable;
 330  0
         fireStateChanged();
 331  0
     }
 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  0
         List<WorkListener> temp = new ArrayList<WorkListener>();
 340  0
         temp.addAll(listeners);
 341  
 
 342  0
         if (!temp.contains(li)) {
 343  0
             temp.add(li);
 344  0
             listeners = temp;
 345  
         }
 346  0
     }
 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  0
         if (listeners.contains(li)) {
 355  0
             List<WorkListener> temp = new ArrayList<WorkListener>();
 356  0
             temp.addAll(listeners);
 357  0
             temp.remove(li);
 358  0
             listeners = temp;
 359  
         }
 360  0
     }
 361  
 
 362  
     protected void fireStateChanged() {
 363  0
         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  0
         final List<WorkListener> temp = new ArrayList<WorkListener>();
 369  0
         synchronized (this) {
 370  0
             if (listeners != null) {
 371  0
                 temp.addAll(listeners);
 372  
             }
 373  0
         }
 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  0
         int count = temp.size();
 378  0
         for (int i = 0; i < count; i++) {
 379  0
             temp.get(i).workStateChanged(ev);
 380  
         }
 381  0
     }
 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  0
         int oldPercent = percent;
 391  0
         workUnits = (int) (now - startTime);
 392  
 
 393  
         // Are we taking more time than expected?
 394  
         // Then we are at 100%
 395  0
         if (workUnits > totalUnits) {
 396  0
             workUnits = totalUnits;
 397  0
             percent = 100;
 398  
         } else {
 399  0
             percent = 100 * workUnits / totalUnits;
 400  
         }
 401  0
         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  0
         int maxAge = UNKNOWN;
 411  
         try {
 412  0
             currentPredictionMap = new HashMap<String, Integer>();
 413  0
             PropertyMap temp = NetUtil.loadProperties(predictionMapURI);
 414  
 
 415  
             // Determine the predicted time from the current prediction map
 416  0
             for (String title : temp.keySet()) {
 417  0
                 String timestr = temp.get(title);
 418  
 
 419  
                 try {
 420  0
                     Integer time = Integer.valueOf(timestr);
 421  0
                     currentPredictionMap.put(title, time);
 422  
 
 423  
                     // if this time is later than the latest
 424  0
                     int age = time.intValue();
 425  0
                     if (maxAge < age) {
 426  0
                         maxAge = age;
 427  
                     }
 428  0
                 } catch (NumberFormatException ex) {
 429  0
                     log.error("Time format error", ex);
 430  0
                 }
 431  0
             }
 432  0
         } catch (IOException ex) {
 433  0
             log.debug("Failed to load prediction times - guessing");
 434  0
         }
 435  
 
 436  0
         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  0
         PropertyMap predictions = new PropertyMap();
 446  0
         for (String sectionName : nextPredictionMap.keySet()) {
 447  0
             Integer age = nextPredictionMap.get(sectionName);
 448  0
             predictions.put(sectionName, age.toString());
 449  0
         }
 450  
 
 451  
         // And save. It's not a disaster if this goes wrong
 452  
         try {
 453  0
             NetUtil.storeProperties(predictions, predictionMapURI, "Predicted Startup Times");
 454  0
         } catch (IOException ex) {
 455  0
             log.error("Failed to save prediction times", ex);
 456  0
         }
 457  0
     }
 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  0
         predictionMapURI = null;
 465  0
     }
 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  0
         return jobID;
 556  
     }
 557  
 
 558  
     /**
 559  
      * So we can fake progress for Jobs that don't tell us how they are doing
 560  
      */
 561  0
     final class PredictTask extends TimerTask {
 562  
         /* (non-Javadoc)
 563  
          * @see java.util.TimerTask#run()
 564  
          */
 565  
         @Override
 566  
         public void run() {
 567  0
             if (updateProgress(System.currentTimeMillis())) {
 568  0
                 JobManager.fireWorkProgressed(Job.this);
 569  
             }
 570  0
         }
 571  
     }
 572  
 
 573  
     /**
 574  
      * The log stream
 575  
      */
 576  0
     private static final Logger log = LoggerFactory.getLogger(Job.class);
 577  
 }