Print

Print


Author: [log in to unmask]
Date: Wed May 20 14:39:52 2015
New Revision: 3006

Log:
Fix dumb bugs; add javadoc; other minor restructuring.

Added:
    java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/package-info.java
Modified:
    java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/DateFileFilter.java
    java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EpicsLog.java
    java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EventTypeLog.java
    java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileCrawler.java
    java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileFilter.java
    java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileList.java
    java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileSequenceComparator.java
    java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileUtilities.java
    java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileVisitor.java
    java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/JCacheManager.java
    java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/RunFilter.java
    java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/RunLog.java
    java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/RunProcessor.java
    java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/RunSummary.java

Modified: java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/DateFileFilter.java
 =============================================================================
--- java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/DateFileFilter.java	(original)
+++ java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/DateFileFilter.java	Wed May 20 14:39:52 2015
@@ -7,14 +7,34 @@
 import java.nio.file.attribute.BasicFileAttributes;
 import java.util.Date;
 
+/**
+ * Filter a file on its creation date.
+ * <p>
+ * Files with a creation date after the time stamp will be rejected.
+ *
+ * @author Jeremy McCormick
+ */
 final class DateFileFilter implements FileFilter {
 
+    /**
+     * The cut off time stamp.
+     */
     private final Date date;
 
+    /**
+     * Create a filter with the given date as the cut off.
+     *
+     * @param date the time stamp cut off
+     */
     DateFileFilter(final Date date) {
         this.date = date;
     }
 
+    /**
+     * Return <code>true</code> if the file was created before the time stamp date.
+     *
+     * @return <code>true</code> if file was created before the time stamp date
+     */
     @Override
     public boolean accept(final File pathname) {
         BasicFileAttributes attr = null;

Modified: java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EpicsLog.java
 =============================================================================
--- java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EpicsLog.java	(original)
+++ java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EpicsLog.java	Wed May 20 14:39:52 2015
@@ -3,26 +3,55 @@
 import java.util.HashMap;
 import java.util.Map;
 
+import org.hps.record.epics.EpicsData;
 import org.hps.record.epics.EpicsEvioProcessor;
-import org.hps.record.epics.EpicsData;
 import org.hps.record.evio.EvioEventProcessor;
 import org.jlab.coda.jevio.EvioEvent;
 
-public final class EpicsLog extends EvioEventProcessor {
+/**
+ * Create a summary log of EPICS information found in EVIO events.
+ *
+ * @author Jeremy McCormick
+ */
+final class EpicsLog extends EvioEventProcessor {
 
+    /**
+     * A count of how many times a given EPICS variable is found in the input, e.g. for computing the mean value across the run.
+     */
     private final Map<String, Integer> counts = new HashMap<String, Integer>();
 
+    /**
+     * The current EPICS data block from the EVIO events (last one that was found).
+     */
     private EpicsData currentData;
 
+    /**
+     * The summary information for the variables from computing the mean across the whole run.
+     */
     private final EpicsData logData = new EpicsData();
+
+    /**
+     * The processor for extracting the EPICS information from EVIO events.
+     */
     private final EpicsEvioProcessor processor = new EpicsEvioProcessor();
 
+    /**
+     * Reference to the run summary which will contain the EPICs information.
+     */
     private final RunSummary runSummary;
 
+    /**
+     * Create an EPICs log pointing to a run summary.
+     * 
+     * @param runSummary the run summary
+     */
     EpicsLog(final RunSummary runSummary) {
         this.runSummary = runSummary;
     }
 
+    /**
+     * End of job hook which computes the mean values for all EPICS variables found in the run.
+     */
     @Override
     public void endJob() {
         System.out.println(this.logData);
@@ -38,13 +67,21 @@
         this.runSummary.setEpicsData(this.logData);
     }
 
+    /**
+     * Process a single EVIO event, setting the current EPICS data and updating the variable counts.
+     */
     @Override
     public void process(final EvioEvent evioEvent) {
         this.processor.process(evioEvent);
         this.currentData = this.processor.getEpicsScalarData();
-        update();
+        this.update();
     }
 
+    /**
+     * Update state from the current EPICS data.
+     * <p>
+     * If the current data is null, this method does nothing.
+     */
     private void update() {
         if (this.currentData != null) {
             for (final String name : this.currentData.getUsedNames()) {
@@ -59,8 +96,7 @@
                 this.counts.put(name, count);
                 final double value = this.logData.getValue(name) + this.currentData.getValue(name);
                 this.logData.setValue(name, value);
-                System.out.println(name + " => added " + this.currentData.getValue(name) + "; total = " + value
-                        + "; mean = " + value / count);
+                System.out.println(name + " => added " + this.currentData.getValue(name) + "; total = " + value + "; mean = " + value / count);
             }
         }
     }

Modified: java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EventTypeLog.java
 =============================================================================
--- java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EventTypeLog.java	(original)
+++ java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EventTypeLog.java	Wed May 20 14:39:52 2015
@@ -8,11 +8,28 @@
 import org.hps.record.evio.EvioEventProcessor;
 import org.jlab.coda.jevio.EvioEvent;
 
-public class EventTypeLog extends EvioEventProcessor {
+/**
+ * This class makes a log of the number of different event types found in a run by their tag value.
+ *
+ * @author Jeremy McCormick
+ */
+final class EventTypeLog extends EvioEventProcessor {
 
-    Map<Object, Integer> eventTypeCounts = new HashMap<Object, Integer>();
-    RunSummary runSummary;
+    /**
+     * The event tag counts for the run.
+     */
+    private final Map<Object, Integer> eventTypeCounts = new HashMap<Object, Integer>();
 
+    /**
+     * The run summary to update.
+     */
+    private final RunSummary runSummary;
+
+    /**
+     * Create the log pointing to a run summary.
+     *
+     * @param runSummary the run summary
+     */
     EventTypeLog(final RunSummary runSummary) {
         this.runSummary = runSummary;
         for (final EventTagConstant constant : EventTagConstant.values()) {
@@ -23,15 +40,28 @@
         }
     }
 
+    /**
+     * End of job hook which sets the event type counts on the run summary.
+     */
     @Override
     public void endJob() {
         this.runSummary.setEventTypeCounts(this.eventTypeCounts);
     }
 
+    /**
+     * Get the counts of different event types (physics events, PRESTART, etc.).
+     *
+     * @return a map of event types to their counts
+     */
     Map<Object, Integer> getEventTypeCounts() {
         return this.eventTypeCounts;
     }
 
+    /**
+     * Process an EVIO event and add its type to the map.
+     *
+     * @param event the EVIO event
+     */
     @Override
     public void process(final EvioEvent event) {
         for (final EventTagConstant constant : EventTagConstant.values()) {

Modified: java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileCrawler.java
 =============================================================================
--- java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileCrawler.java	(original)
+++ java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileCrawler.java	Wed May 20 14:39:52 2015
@@ -21,32 +21,51 @@
 import org.lcsim.util.log.LogUtil;
 
 /**
- * Crawls EVIO files in a directory tree, groups the files that are found by run, and optionally performs various tasks
- * based on the run summary information that is accumulated, including printing a summary, caching the files from JLAB
- * MSS, and updating a run database.
+ * Crawls EVIO files in a directory tree, groups the files that are found by run, and optionally performs various tasks based on the run summary
+ * information that is accumulated, including printing a summary, caching the files from JLAB MSS, and updating a run database.
  *
  * @author <a href="mailto:[log in to unmask]">Jeremy McCormick</a>
  */
 public final class EvioFileCrawler {
 
-    private static final Logger LOGGER = LogUtil.create(EvioFileVisitor.class, new DefaultLogFormatter(), Level.ALL);
-
+    /**
+     * Setup logger.
+     */
+    private static final Logger LOGGER = LogUtil.create(EvioFileCrawler.class, new DefaultLogFormatter(), Level.ALL);
+
+    /**
+     * Constant for milliseconds conversion.
+     */
+    private static final long MILLISECONDS = 1000L;
+
+    /**
+     * Command line options for the crawler.
+     */
     private static final Options OPTIONS = new Options();
 
+    /**
+     * Statically define the command options.
+     */
     static {
         OPTIONS.addOption("h", "help", false, "print help and exit");
-        OPTIONS.addOption("t", "timestamp-file", true,
-                "timestamp file for date filtering; modified time will be set at end of job");
+        OPTIONS.addOption("t", "timestamp-file", true, "timestamp file for date filtering; modified time will be set at end of job");
         OPTIONS.addOption("d", "directory", true, "starting directory");
         OPTIONS.addOption("r", "runs", true, "list of runs to accept (others will be excluded)");
         OPTIONS.addOption("s", "summary", false, "print run summary at end of job");
         OPTIONS.addOption("L", "log-level", true, "set log level (INFO, FINE, etc.)");
         OPTIONS.addOption("u", "update", false, "update the run database");
         OPTIONS.addOption("e", "epics", false, "process EPICS data");
-        OPTIONS.addOption("c", "cache", false, "cache all files from MSS");
-        OPTIONS.addOption("w", "wait", true, "total time in ms for file caching");
-    }
-
+        OPTIONS.addOption("c", "cache", false, "automatically cache all files from MSS");
+        OPTIONS.addOption("w", "wait", true, "total time in seconds to allow for file caching");
+        OPTIONS.addOption("m", "max-files", true, "maximum number of files to accept per run (for debugging)");
+        OPTIONS.addOption("p", "print", true, "set event printing interval when running EVIO processors");
+    }
+
+    /**
+     * Support running the crawler from the command line.
+     *
+     * @param args the command line arguments
+     */
     public static void main(final String[] args) {
         try {
             new EvioFileCrawler().parse(args).run();
@@ -55,52 +74,110 @@
         }
     }
 
+    /**
+     * A list of run numbers to accept in the job.
+     */
     private final Set<Integer> acceptRuns = new HashSet<Integer>();
 
+    /**
+     * The class for managing the file caching using the jcache command.
+     */
+    private final JCacheManager cacheManager = new JCacheManager();
+
+    /**
+     * Default event print interval.
+     */
+    private final int DEFAULT_EVENT_PRINT_INTERVAL = 1000;
+
+    /**
+     * Flag indicating whether EPICS data banks should be processed.
+     */
     private boolean epics = false;
 
+    /**
+     * Interval for printing out event number while running EVIO processors.
+     */
+    private int eventPrintInterval = DEFAULT_EVENT_PRINT_INTERVAL;
+
+    /**
+     * The maximum number of files to
+     */
+    private int maxFiles = -1;
+
+    /**
+     * The options parser.
+     */
     private final PosixParser parser = new PosixParser();
 
+    /**
+     * Flag indicating whether the run summaries should be printed (may result in some extra file processing).
+     */
     private boolean printSummary = false;
 
+    /**
+     * The root directory to crawl which defaults to the current directory.
+     */
     private File rootDir = new File(System.getProperty("user.dir"));
 
+    /**
+     * A timestamp to use for filtering input files on their creation date.
+     */
     private Date timestamp = null;
 
+    /**
+     * A file to use for the timestamp date.
+     */
     private File timestampFile = null;
 
+    /**
+     * Flag indicating if the run database should be updated from results of the job.
+     */
     private boolean update = false;
-    
-    private boolean cache = false;
-    
-    private Long waitTime; 
-
+
+    /**
+     * Flag indicating if the file cache should be used (e.g. jcache automatically executed to move files to the cache disk from tape).
+     */
+    private boolean useFileCache = false;
+
+    /**
+     * The maximum wait time in milliseconds to allow for file caching operations.
+     */
+    private Long waitTime;
+
+    /**
+     * Create the processor for a single run.
+     *
+     * @param runSummary the run summary for the run
+     * @return the run processor
+     */
     private RunProcessor createRunProcessor(final RunSummary runSummary) {
-        final RunProcessor processor = new RunProcessor(runSummary);
+        final RunProcessor processor = new RunProcessor(runSummary, this.cacheManager);
         if (this.epics) {
             processor.addProcessor(new EpicsLog(runSummary));
         }
         if (this.printSummary) {
             processor.addProcessor(new EventTypeLog(runSummary));
         }
+        if (this.maxFiles != -1) {
+            processor.setMaxFiles(this.maxFiles);
+        }
+        processor.useFileCache(this.useFileCache);
+        processor.setEventPrintInterval(this.eventPrintInterval);
         return processor;
     }
-    
-    /**
-     * Print the usage statement for this tool to the console.
-     */
-    private final void printUsage() {
-        final HelpFormatter help = new HelpFormatter();
-        help.printHelp("EvioFileCrawler", "", OPTIONS, "");
-        System.exit(0);
-    }
-
+
+    /**
+     * Parse command line options, but do not start the job.
+     *
+     * @param args the command line arguments
+     * @return the configured crawler object
+     */
     private EvioFileCrawler parse(final String args[]) {
         try {
             final CommandLine cl = this.parser.parse(OPTIONS, args);
-            
+
             if (cl.hasOption("h")) {
-                printUsage();
+                this.printUsage();
             }
 
             if (cl.hasOption("L")) {
@@ -122,12 +199,10 @@
             if (cl.hasOption("t")) {
                 this.timestampFile = new File(cl.getOptionValue("t"));
                 if (!this.timestampFile.exists()) {
-                    throw new IllegalArgumentException("The timestamp file does not exist: "
-                            + this.timestampFile.getPath());
+                    throw new IllegalArgumentException("The timestamp file does not exist: " + this.timestampFile.getPath());
                 }
                 try {
-                    this.timestamp = new Date(Files
-                            .readAttributes(this.timestampFile.toPath(), BasicFileAttributes.class).lastModifiedTime()
+                    this.timestamp = new Date(Files.readAttributes(this.timestampFile.toPath(), BasicFileAttributes.class).lastModifiedTime()
                             .toMillis());
                 } catch (final IOException e) {
                     throw new RuntimeException("Error getting attributes of timestamp file.", e);
@@ -149,17 +224,28 @@
             if (cl.hasOption("u")) {
                 this.update = true;
             }
-           
+
             if (cl.hasOption("e")) {
                 this.epics = true;
             }
-            
+
             if (cl.hasOption("c")) {
-                this.cache = true;
-            }
-            
+                this.useFileCache = true;
+            }
+
             if (cl.hasOption("w")) {
-                this.waitTime = Long.parseLong(cl.getOptionValue("w"));
+                this.waitTime = Long.parseLong(cl.getOptionValue("w")) * MILLISECONDS;
+                if (this.waitTime > 0L) {
+                    this.cacheManager.setWaitTime(this.waitTime);
+                }
+            }
+
+            if (cl.hasOption("m")) {
+                this.maxFiles = Integer.parseInt(cl.getOptionValue("m"));
+            }
+
+            if (cl.hasOption("p")) {
+                this.eventPrintInterval = Integer.parseInt(cl.getOptionValue("p"));
             }
 
         } catch (final ParseException e) {
@@ -169,63 +255,54 @@
         return this;
     }
 
-    private void cacheFiles(final RunLog runs) {
-        
-        JCacheManager cache = new JCacheManager();
-        
-        if (this.waitTime != null) {
-            LOGGER.config("set jcache wait time to " + waitTime + " millis");
-            cache.setWaitTime(waitTime);
-        }
-        
-        // Process all files in the runs.
+    /**
+     * Print the usage statement for this tool to the console.
+     */
+    private void printUsage() {
+        final HelpFormatter help = new HelpFormatter();
+        help.printHelp("EvioFileCrawler", "", OPTIONS, "");
+        System.exit(0);
+    }
+
+    /**
+     * Process a single run.
+     *
+     * @param runSummary the run summary information
+     * @throws Exception if there is some error while running the file processing
+     */
+    private void processRun(final RunSummary runSummary) throws Exception {
+
+        // Clear the cache manager.
+        this.cacheManager.clear();
+
+        // Create a processor to process all the EVIO events in the run.
+        final RunProcessor processor = this.createRunProcessor(runSummary);
+
+        // Process all of the runs files.
+        processor.process();
+    }
+
+    /**
+     * Process all runs that were found.
+     *
+     * @param runs the run log containing the list of run summaries
+     * @throws Exception if there is an error processing one of the runs
+     */
+    private void processRuns(final RunLog runs) throws Exception {
+        // Process all of the runs that were found.
         for (final int run : runs.getSortedRunNumbers()) {
-            
-            LOGGER.info("processing run " + run + " files ...");
-                        
-            // Get the run summary for the run.
-            final RunSummary runSummary = runs.getRunSummary(run);
-            
-            // Cache all the files.
-            LOGGER.info("caching " + runSummary.getFiles().size() + " files ...");           
-            cache.cache(runSummary.getFiles());
-                        
-            // Wait for cache operation to complete.
-            boolean cached = cache.waitForAll();
-            
-            LOGGER.info("files were cached: " + cached);
-            
-            if (!cached) {
-                throw new RuntimeException("The cache operation did not complete in time.");
-            }
-            
-            LOGGER.info("done caching run " + run);
-        }
-    }
-    
-    private void processRuns(final RunLog runs) throws Exception {
-                        
-        // Process all files in the runs.
-        for (final int run : runs.getSortedRunNumbers()) {
-            
-            LOGGER.info("processing run " + run + " ...");
-                        
-            // Get the run summary for the run.
-            final RunSummary runSummary = runs.getRunSummary(run);
-                        
-            // Create a processor to process all the EVIO records in the run.
-            final RunProcessor processor = createRunProcessor(runSummary);            
-                                   
-            // Process the run, updating the run summary.
-            processor.process();
-            
-            LOGGER.info("done processing run " + run);
-        }
-    }
-
+            this.processRun(runs.getRunSummary(run));
+        }
+    }
+
+    /**
+     * Run the crawler job (generally will take a long time!).
+     *
+     * @throws Exception if there is some error during the job
+     */
     public void run() throws Exception {
         final EnumSet<FileVisitOption> options = EnumSet.noneOf(FileVisitOption.class);
-        final EvioFileVisitor visitor = new EvioFileVisitor();
+        final EvioFileVisitor visitor = new EvioFileVisitor(this.timestamp);
         if (this.timestamp != null) {
             visitor.addFilter(new DateFileFilter(this.timestamp));
             LOGGER.config("added date filter with timestamp " + this.timestamp);
@@ -242,25 +319,20 @@
         }
 
         final RunLog runs = visitor.getRunLog();
-        
+
         // Print run numbers that were found.
-        StringBuffer sb = new StringBuffer();
-        for (Integer run : runs.getSortedRunNumbers()) {
+        final StringBuffer sb = new StringBuffer();
+        for (final Integer run : runs.getSortedRunNumbers()) {
             sb.append(run + " ");
         }
         LOGGER.info("found files from runs: " + sb.toString());
-        
+
         // Sort files on their sequence numbers.
         LOGGER.fine("sorting files by sequence ...");
         runs.sortAllFiles();
-        
-        // Cache all the files to disk before processing them.
-        if (this.cache) {
-            cacheFiles(runs);
-        }
-
-        // Process all the files in the runs.
-        processRuns(runs);
+
+        // Process all the files in all of runs. This will perform caching from MSS if necessary.
+        this.processRuns(runs);
 
         // Print the run summary information.
         if (this.printSummary) {
@@ -273,7 +345,7 @@
             runs.insert();
         }
 
-        // Update the timestamp file.
+        // Update the timestamp file which can be used to tell which files have been processed.
         if (this.timestampFile == null) {
             this.timestampFile = new File("timestamp");
             try {

Modified: java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileFilter.java
 =============================================================================
--- java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileFilter.java	(original)
+++ java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileFilter.java	Wed May 20 14:39:52 2015
@@ -3,16 +3,27 @@
 import java.io.File;
 import java.io.FileFilter;
 
+/**
+ * This is a simple file filter that will accept EVIO files with a certain convention to their naming which looks like <i>FILENAME.evio.SEQUENCE</i>.
+ * This matches the convention used by the CODA DAQ software.
+ *
+ * @author Jeremy McCormick
+ */
 final class EvioFileFilter implements FileFilter {
 
-    @Override    
+    /**
+     * Return <code>true</code> if file is an EVIO file with correct file name convention.
+     *
+     * @return <code>true</code> if file is an EVIO file with correct file name convention
+     */
+    @Override
     public boolean accept(final File pathname) {
-        boolean isEvio = pathname.getName().contains(".evio");
+        final boolean isEvio = pathname.getName().contains(".evio");
         boolean hasSeqNum = false;
         try {
             EvioFileUtilities.getSequenceNumber(pathname);
             hasSeqNum = true;
-        } catch (Exception e) {
+        } catch (final Exception e) {
         }
         return isEvio && hasSeqNum;
     }

Modified: java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileList.java
 =============================================================================
--- java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileList.java	(original)
+++ java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileList.java	Wed May 20 14:39:52 2015
@@ -1,7 +1,6 @@
 package org.hps.record.evio.crawler;
 
 import java.io.File;
-import java.io.IOException;
 import java.sql.Connection;
 import java.sql.PreparedStatement;
 import java.sql.SQLException;
@@ -12,41 +11,42 @@
 import java.util.Map;
 import java.util.logging.Logger;
 
-import org.jlab.coda.jevio.EvioException;
-import org.jlab.coda.jevio.EvioReader;
 import org.lcsim.util.log.LogUtil;
 
+/**
+ * This is a list of <code>File</code> objects that are assumed to be EVIO files. There are some added utilities for getting the total number of
+ * events in all the files.
+ *
+ * @author Jeremy McCormick
+ */
 final class EvioFileList extends ArrayList<File> {
 
+    /**
+     * Setup logger.
+     */
     private static final Logger LOGGER = LogUtil.create(EvioFileList.class);
 
-    Map<File, Integer> eventCounts = new HashMap<File, Integer>();
+    /**
+     * Event count by file.
+     */
+    private final Map<File, Integer> eventCounts = new HashMap<File, Integer>();
 
-    void computeEventCount(EvioReader reader, final File file) throws IOException, EvioException {
-        if (!reader.getPath().equals(file.getPath())) {
-            throw new IllegalArgumentException("The EvioReader and file paths do not match.");
-        }        
-        LOGGER.info("computing event count for " + file.getPath() + " ...");
-        int eventCount = reader.getEventCount();
-        this.eventCounts.put(file, eventCount);
-        LOGGER.info("done computing event count for " + file.getPath());
-    }
-
-    int computeTotalEvents() {
-        LOGGER.info("computing total events ...");
-        int totalEvents = 0;
-        for (final File file : this) {
-            getEventCount(file);
-            totalEvents += this.eventCounts.get(file);
-        }
-        LOGGER.info("done computing total events");
-        return totalEvents;
-    }
-
+    /**
+     * Get the first file.
+     *
+     * @return the first file
+     */
     File first() {
         return this.get(0);
     }
 
+    /**
+     * Get the event count for an EVIO file.
+     *
+     * @param file the EVIO file
+     * @return the event count for the file
+     * @throws RuntimeException if the count was never computed (file is not in map)
+     */
     int getEventCount(final File file) {
         if (this.eventCounts.get(file) == null) {
             throw new RuntimeException("The event count for " + file.getPath() + " was never computed.");
@@ -54,11 +54,38 @@
         return this.eventCounts.get(file);
     }
 
+    /**
+     * Get the total number of events.
+     * <p>
+     * Files which do not have their event counts computed will be ignored.
+     *
+     * @return the total number of events
+     */
+    int getTotalEvents() {
+        int totalEvents = 0;
+        for (final File file : this) {
+            if (this.eventCounts.containsKey(file)) {
+                totalEvents += this.eventCounts.get(file);
+            } else {
+                // Warn about non-computed count.
+                // FIXME: Perhaps this should actually be a fatal error.
+                LOGGER.warning("event count for " + file.getPath() + " was not computed and will not be reflected in total");
+            }
+        }
+        return totalEvents;
+    }
+
+    /**
+     * Insert the file names into the run database.
+     *
+     * @param connection the database connection
+     * @param run the run number
+     * @throws SQLException if there is a problem executing one of the database queries
+     */
     void insert(final Connection connection, final int run) throws SQLException {
         LOGGER.info("updating file list ...");
         PreparedStatement filesStatement = null;
-        filesStatement = connection
-                .prepareStatement("INSERT INTO run_log_files (run, directory, name) VALUES(?, ?, ?)");
+        filesStatement = connection.prepareStatement("INSERT INTO run_log_files (run, directory, name) VALUES(?, ?, ?)");
         LOGGER.info("inserting files from run " + run + " into database");
         for (final File file : this) {
             LOGGER.info("creating update statement for " + file.getPath());
@@ -71,10 +98,28 @@
         LOGGER.info("run_log_files was updated!");
     }
 
+    /**
+     * Get the last file.
+     *
+     * @return the last file
+     */
     File last() {
         return this.get(this.size() - 1);
     }
 
+    /**
+     * Set the event count for a file.
+     *
+     * @param file the EVIO file
+     * @param eventCount the event count
+     */
+    void setEventCount(final File file, final Integer eventCount) {
+        this.eventCounts.put(file, eventCount);
+    }
+
+    /**
+     * Sort the files in-place by their sequence number.
+     */
     void sort() {
         final List<File> fileList = new ArrayList<File>(this);
         Collections.sort(fileList, new EvioFileSequenceComparator());

Modified: java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileSequenceComparator.java
 =============================================================================
--- java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileSequenceComparator.java	(original)
+++ java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileSequenceComparator.java	Wed May 20 14:39:52 2015
@@ -3,8 +3,18 @@
 import java.io.File;
 import java.util.Comparator;
 
+/**
+ * Compare two EVIO files by their sequence numbers.
+ *
+ * @author Jeremy McCormick
+ */
 final class EvioFileSequenceComparator implements Comparator<File> {
 
+    /**
+     * Compare two EVIO files by their sequence numbers.
+     *
+     * @return -1 if the first file's sequence number is less than the second's; 0 if equal; 1 if greater than
+     */
     @Override
     public int compare(final File o1, final File o2) {
         final Integer sequenceNumber1 = EvioFileUtilities.getSequenceNumber(o1);

Modified: java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileUtilities.java
 =============================================================================
--- java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileUtilities.java	(original)
+++ java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileUtilities.java	Wed May 20 14:39:52 2015
@@ -13,12 +13,48 @@
 import org.jlab.coda.jevio.EvioReader;
 import org.lcsim.util.log.LogUtil;
 
-public final class EvioFileUtilities {
-
+/**
+ * A miscellaneous collection of EVIO file utility methods used by classes in the crawler package.
+ *
+ * @author Jeremy McCormick
+ */
+final class EvioFileUtilities {
+
+    /**
+     * Setup logger.
+     */
     private static final Logger LOGGER = LogUtil.create(EvioFileUtilities.class);
 
+    /**
+     * Milliseconds constant for conversion to/from second.
+     */
     private static final long MILLISECONDS = 1000L;
 
+    /**
+     * Get a cached file path assuming that the input file is on the JLAB MSS.
+     *
+     * @param file the MSS file path
+     * @return the cached file path
+     * @throws IllegalArgumentException if the file is not on the MSS (e.g. path does not start with "/mss")
+     */
+    static File getCachedFile(final File file) {
+        if (!isMssFile(file)) {
+            throw new IllegalArgumentException("File " + file.getPath() + " is not on the JLab MSS.");
+        }
+        if (isCachedFile(file)) {
+            throw new IllegalArgumentException("File " + file.getPath() + " is already on the cache disk.");
+        }
+        return new File("/cache" + file.getPath());
+    }
+
+    /**
+     * Get the date from the control bank of an EVIO event.
+     *
+     * @param file the EVIO file
+     * @param eventTag the event tag on the bank
+     * @param gotoEvent an event to start the scanning
+     * @return the control bank date or null if not found
+     */
     static Date getControlDate(final File file, final int eventTag, final int gotoEvent) {
         Date date = null;
         EvioReader reader = null;
@@ -52,6 +88,12 @@
         return date;
     }
 
+    /**
+     * Get the date from the head bank.
+     *
+     * @param event the EVIO file
+     * @return the date from the head bank or null if not found
+     */
     static Date getHeadBankDate(final EvioEvent event) {
         Date date = null;
         final BaseStructure headBank = EvioEventUtilities.getHeadBank(event);
@@ -65,6 +107,12 @@
         return date;
     }
 
+    /**
+     * Get the run end date which is taken either from the END event or the last physics event is the END event is not found.
+     *
+     * @param file the EVIO file
+     * @return the run end date
+     */
     static Date getRunEnd(final File file) {
         Date date = getControlDate(file, EvioEventConstants.END_EVENT_TAG, -10);
         if (date == null) {
@@ -95,6 +143,13 @@
         return date;
     }
 
+    /**
+     * Get the run number from the file name.
+     *
+     * @param file the EVIO file
+     * @return the run number
+     * @throws Exception if there is a problem parsing out the run number
+     */
     static Integer getRunFromName(final File file) {
         final String name = file.getName();
         final int startIndex = name.lastIndexOf("_") + 1;
@@ -102,6 +157,14 @@
         return Integer.parseInt(name.substring(startIndex, endIndex));
     }
 
+    /**
+     * Get the run start date from an EVIO file (should be the first in the run).
+     * <p>
+     * This is taken from the PRESTART event.
+     *
+     * @param file the EVIO file
+     * @return the run start date
+     */
     static Date getRunStart(final File file) {
         Date date = getControlDate(file, EvioEventConstants.PRESTART_EVENT_TAG, 0);
         if (date == null) {
@@ -131,46 +194,80 @@
         return date;
     }
 
+    /**
+     * Get the EVIO file sequence number (the number at the end of the file name).
+     *
+     * @param file the EVIO file
+     * @return the file's sequence number
+     * @throws Exception if there is an error parsing out the sequence number
+     */
     static Integer getSequenceNumber(final File file) {
         final String name = file.getName();
         return Integer.parseInt(name.substring(name.lastIndexOf(".") + 1));
     }
 
+    /**
+     * Return <code>true</code> if this is a cached file e.g. the path starts with "/cache".
+     *
+     * @param file the file
+     * @return <code>true</code> if the file is a cached file
+     */
+    static boolean isCachedFile(final File file) {
+        return file.getPath().startsWith("/cache");
+    }
+
+    /**
+     * Return <code>true</code> if this file is on the JLAB MSS e.g. the path starts with "/mss".
+     *
+     * @param file the file
+     * @return <code>true</code> if the file is on the MSS
+     */
+    static boolean isMssFile(final File file) {
+        return file.getPath().startsWith("/mss");
+    }
+
+    /**
+     * Open an EVIO file using a <code>EvioReader</code>.
+     *
+     * @param file the EVIO file
+     * @return the new <code>EvioReader</code> for the file
+     * @throws IOException if there is an IO problem
+     * @throws EvioException if there is an error reading the EVIO data
+     */
     static EvioReader open(final File file) throws IOException, EvioException {
         return open(file, false);
     }
-    
-    static EvioReader open(final File file, boolean sequential) throws IOException, EvioException {
+
+    /**
+     * Open an EVIO file, using the cached file path if necessary.
+     *
+     * @param file the EVIO file
+     * @param sequential <code>true</code> to enable sequential reading
+     * @return the new <code>EvioReader</code> for the file
+     * @throws IOException if there is an IO problem
+     * @throws EvioException if there is an error reading the EVIO data
+     */
+    static EvioReader open(final File file, final boolean sequential) throws IOException, EvioException {
         File openFile = file;
         if (isMssFile(file)) {
             openFile = getCachedFile(file);
-        }        
+        }
         final long start = System.currentTimeMillis();
         final EvioReader reader = new EvioReader(openFile, false, sequential);
         final long end = System.currentTimeMillis() - start;
         LOGGER.info("opened " + openFile.getPath() + " in " + end / MILLISECONDS + " seconds");
         return reader;
     }
-    
+
+    /**
+     * Open an EVIO from a path.
+     *
+     * @param path the file path
+     * @return the new <code>EvioReader</code> for the file
+     * @throws IOException if there is an IO problem
+     * @throws EvioException if there is an error reading the EVIO data
+     */
     static EvioReader open(final String path) throws IOException, EvioException {
         return open(new File(path));
-    }    
-    
-    static File getCachedFile(File file) {
-        if (!isMssFile(file)) {
-            throw new IllegalArgumentException("File " + file.getPath() + " is not on the JLab MSS.");
-        }
-        if (isCachedFile(file)) {
-            throw new IllegalArgumentException("File " + file.getPath() + " is already on the cache disk.");
-        }
-        return new File("/cache" + file.getPath());
-    }
-    
-    static boolean isMssFile(File file) {
-        return file.getPath().startsWith("/mss");
-    }
-    
-    static boolean isCachedFile(File file) {
-        return file.getPath().startsWith("/cache");
     }
 }

Modified: java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileVisitor.java
 =============================================================================
--- java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileVisitor.java	(original)
+++ java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/EvioFileVisitor.java	Wed May 20 14:39:52 2015
@@ -7,60 +7,112 @@
 import java.nio.file.SimpleFileVisitor;
 import java.nio.file.attribute.BasicFileAttributes;
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import org.lcsim.util.log.DefaultLogFormatter;
 import org.lcsim.util.log.LogUtil;
 
+/**
+ * A file visitor that crawls directories looking for EVIO files.
+ *
+ * @author Jeremy McCormick
+ */
 final class EvioFileVisitor extends SimpleFileVisitor<Path> {
 
-    private static final Logger LOGGER = LogUtil.create(EvioFileVisitor.class);
+    /**
+     * Setup logger.
+     */
+    private static final Logger LOGGER = LogUtil.create(EvioFileVisitor.class, new DefaultLogFormatter(), Level.FINE);
 
+    /**
+     * A list of file filters to apply.
+     */
     private final List<FileFilter> filters = new ArrayList<FileFilter>();
 
+    /**
+     * The run log containing information about files from each run.
+     */
     private final RunLog runs = new RunLog();
 
-    EvioFileVisitor() {
-        addFilter(new EvioFileFilter());
+    /**
+     * Create a new file visitor.
+     *
+     * @param timestamp the timestamp which is used for date filtering
+     */
+    EvioFileVisitor(final Date timestamp) {
+        this.addFilter(new EvioFileFilter());
+        if (timestamp != null) {
+            // Add date filter if timestamp is supplied.
+            this.addFilter(new DateFileFilter(timestamp));
+        }
     }
 
+    /**
+     * Run the filters on the file to tell whether it should be accepted or not.
+     *
+     * @param file the EVIO file
+     * @return <code>true</code> if file should be accepted
+     */
     private boolean accept(final File file) {
         boolean accept = true;
         for (final FileFilter filter : this.filters) {
             accept = filter.accept(file);
             if (accept == false) {
-                LOGGER.fine(filter.getClass().getSimpleName() + " rejected file: " + file.getPath());
+                LOGGER.finer(filter.getClass().getSimpleName() + " rejected " + file.getPath());
                 break;
             }
         }
         return accept;
     }
 
+    /**
+     * Add a file filter.
+     *
+     * @param filter the file filter
+     */
     void addFilter(final FileFilter filter) {
         this.filters.add(filter);
-        LOGGER.config("added filter: " + filter.getClass().getSimpleName());
+        LOGGER.config("added " + filter.getClass().getSimpleName() + " filter");
     }
 
+    /**
+     * Get the run log.
+     *
+     * @return the run log
+     */
     RunLog getRunLog() {
         return this.runs;
     }
 
+    /**
+     * Visit a single file.
+     *
+     * @param path the file to visit
+     * @param attrs the file attributes
+     */
     @Override
     public FileVisitResult visitFile(final Path path, final BasicFileAttributes attrs) {
+        final File file = path.toFile();
+        if (this.accept(file)) {
 
-        final File file = path.toFile();
-        if (accept(file)) {
+            // Get the run number from the file name.
+            final Integer run = EvioFileUtilities.getRunFromName(file);
 
-            final Integer run = EvioFileUtilities.getRunFromName(file);
+            // Get the sequence number from the file name.
             final Integer seq = EvioFileUtilities.getSequenceNumber(file);
 
-            LOGGER.info("adding file: " + file.getPath() + "; run: " + run + "; seq = " + seq);
+            LOGGER.info("accepted file " + file.getPath() + " with run " + run + " and seq " + seq);
 
+            // Add this file to the file list for the run.
             this.runs.getRunSummary(run).addFile(file);
         } else {
-            LOGGER.info("rejected file: " + file.getPath());
+            // File was rejected by one of the filters.
+            LOGGER.finer("rejected file " + file.getPath());
         }
+        // Always continue crawling.
         return FileVisitResult.CONTINUE;
     }
 }

Modified: java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/JCacheManager.java
 =============================================================================
--- java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/JCacheManager.java	(original)
+++ java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/JCacheManager.java	Wed May 20 14:39:52 2015
@@ -21,109 +21,420 @@
 
 /**
  * Utility class for using the <i>jcache</i> command at JLAB.
+ *
+ * @author Jeremy McCormick
  */
-public class JCacheManager {
-
-    private static Logger LOGGER = LogUtil.create(JCacheManager.class, new DefaultLogFormatter(), Level.ALL);
-    
-    private Map<File, FileInfo> fileInfos = new HashMap<File, FileInfo>();
-    
-    private static long DEFAULT_MAX_WAIT_TIME = 300000;
-    
-    private long maxWaitTime = DEFAULT_MAX_WAIT_TIME;
-    
-    private static final long POLL_WAIT_TIME = 10000;
-            
-    static class FileInfo {
-
+final class JCacheManager {
+
+    /**
+     * Keeps track of cache status for a single file.
+     *
+     * @author Jeremy McCormick
+     */
+    static class CacheStatus {
+
+        /**
+         * Flag indicating if file is cached yet.
+         */
+        private boolean cached = false;
+
+        /**
+         * Path to the file on the MSS (not the cached path).
+         */
+        private File file = null;
+
+        /**
+         * The request ID from the 'jcache submit' command.
+         */
         private Integer requestId = null;
-        private File file = null;
-        private boolean cached = false;
-
-        FileInfo(File file, Integer requestId) {
+
+        /**
+         * The current status from executing the 'jcache request' command.
+         */
+        private String status;
+
+        /**
+         * Create a new <code>CacheStatus</code> object.
+         *
+         * @param file the file which has the MSS path
+         * @param requestId the request ID from running the cache command
+         */
+        CacheStatus(final File file, final Integer requestId) {
+            this.file = file;
             this.requestId = requestId;
         }
 
-        File getCachedFile() {
-            return new File("/cache" + file.getPath());
-        }
-
+        /**
+         * Get the file (path on MSS).
+         *
+         * @return the file with path on the MSS
+         */
+        File getFile() {
+            return this.file;
+        }
+
+        /**
+         * Get the request ID.
+         *
+         * @return the request ID
+         */
         Integer getRequestId() {
-            return requestId;
-        }
-                
+            return this.requestId;
+        }
+
+        /**
+         * Get the request XML from running 'jcache request' to get the status.
+         *
+         * @param is the input stream from the process
+         * @return the request status string
+         */
+        private Element getRequestXml(final InputStream is) {
+            String xmlString = null;
+            try {
+                xmlString = readFully(is, "US-ASCII");
+            } catch (final IOException e) {
+                throw new RuntimeException(e);
+            }
+            // LOGGER.finer("raw XML: " + xmlString);
+            xmlString = xmlString.substring(xmlString.trim().indexOf("<?xml") + 1);
+            // LOGGER.finer("cleaned XML: " + xmlString);
+            return buildDocument(xmlString).getRootElement();
+        }
+
+        /**
+         * Get the status string.
+         *
+         * @param updateStatus <code>true</code> to run the 'jcache status' command
+         * @return the status string
+         */
+        String getStatus(final boolean updateStatus) {
+            if (updateStatus) {
+                this.update();
+            }
+            return this.status;
+        }
+
+        /**
+         * Return <code>true</code> if file is cached.
+         *
+         * @return <code>true</code> if file is cached
+         */
         boolean isCached() {
-            return cached;
-        }
-                
-        void update() {
-            if (!isCached()) {
-                if (!isPending() && getCachedFile().exists()) {
-                    LOGGER.info("file " + file.getPath() + " is cached");
-                    this.cached = true;
-                }
-            }
-        }
-
-        String getStatus() {
+            return this.cached;
+        }
+
+        /**
+         * Return </code>true</code> if status is "done".
+         *
+         * @return </code>true</code> if status is "done"
+         */
+        boolean isDone() {
+            return "done".equals(this.status);
+        }
+
+        /**
+         * Return </code>true</code> if status is "hit".
+         *
+         * @return </code>true</code> if status is "hit"
+         */
+        boolean isHit() {
+            return "hit".equals(this.status);
+        }
+
+        /**
+         * Return </code>true</code> if status is "pending".
+         *
+         * @return </code>true</code> if status is "pending"
+         */
+        boolean isPending() {
+            return "pending".equals(this.status);
+        }
+
+        /**
+         * Request the file status string using the 'jcache request' command.
+         *
+         * @return the file status string
+         */
+        private String requestFileStatus() {
             Process process = null;
             try {
-                process = new ProcessBuilder("jcache", "request", requestId.toString()).start();
+                process = new ProcessBuilder(JCACHE_COMMAND, "request", this.requestId.toString()).start();
             } catch (final IOException e) {
                 throw new RuntimeException(e);
-            }            
+            }
             int status = 0;
             try {
                 status = process.waitFor();
-            } catch (InterruptedException e) {
-                throw new RuntimeException("Process was interrupted.", e);
+            } catch (final InterruptedException e) {
+                throw new RuntimeException("Cache process was interrupted.", e);
             }
             if (status != 0) {
-                throw new RuntimeException("The jcache request process returned a non-zero exit status: " + status);
-            }            
-            return getRequestXml(process.getInputStream()).getChild("request").getChildText("status");
-        }
-        
-        private Element getRequestXml(InputStream is) {
-            String xmlString = null;
-            try {
-                xmlString = readFully(is, "US-ASCII");
-            } catch (IOException e) {
-                throw new RuntimeException(e);
-            }            
-            xmlString = xmlString.substring(xmlString.trim().indexOf("<?xml"));
-            LOGGER.info(xmlString);
-            return buildDocument(xmlString).getRootElement();
-        }
-                                            
-        boolean isPending() {
-            return !"pending".equals(getStatus());
-        }
-        
-        boolean isDone() {
-            return "done".equals(getStatus());
-        }
-        
-        boolean isHit() {
-            return "hit".equals(getStatus());
-        }
-    }
-    
-    void setWaitTime(long maxWaitTime) {
-        this.maxWaitTime = maxWaitTime;
-    }
-    
-    void cache(List<File> files) {
-        for (File file : files) {
-            cache(file);
-        }
-    }   
-
-    void cache(File file) {
+                throw new RuntimeException("The jcache request returned an error status: " + status);
+            }
+            return this.getRequestXml(process.getInputStream()).getChild("request").getChild("file").getChildText("status");
+        }
+
+        /**
+         * Update the cache status.
+         */
+        void update() {
+            this.status = this.requestFileStatus();
+            if (this.isDone() || this.isHit()) {
+                this.cached = true;
+            }
+        }
+    }
+
+    /**
+     * The default max wait time in milliseconds for all file caching operations to complete (default is ~5 minutes).
+     */
+    private static long DEFAULT_MAX_WAIT_TIME = 300000;
+
+    /**
+     * The command for running jcache (accessible from ifarm machines and batch notes).
+     */
+    private static final String JCACHE_COMMAND = "/site/bin/jcache";
+
+    /**
+     * Setup the logger.
+     */
+    private static Logger LOGGER = LogUtil.create(JCacheManager.class, new DefaultLogFormatter(), Level.FINE);
+
+    /**
+     * Time to wait between polling of all files (~10 seconds).
+     */
+    private static final long POLL_WAIT_TIME = 10000;
+
+    /**
+     * Build an XML document from a string.
+     *
+     * @param xmlString the raw XML string
+     * @return the XML document
+     */
+    private static Document buildDocument(final String xmlString) {
+        final SAXBuilder builder = new SAXBuilder();
+        Document document = null;
+        try {
+            document = builder.build(new InputSource(new StringReader(xmlString)));
+        } catch (final Exception e) {
+            throw new RuntimeException("Error building XML doc.", e);
+        }
+        return document;
+    }
+
+    /**
+     * Read bytes from an input stream into an array.
+     *
+     * @param inputStream the input stream
+     * @return the bytes read
+     * @throws IOException if there is a problem reading the stream
+     */
+    private static byte[] readFully(final InputStream inputStream) throws IOException {
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        final byte[] buffer = new byte[1024];
+        int length = 0;
+        while ((length = inputStream.read(buffer)) != -1) {
+            baos.write(buffer, 0, length);
+        }
+        return baos.toByteArray();
+    }
+
+    /**
+     * Read bytes from an input stream into a string with a certain encoding.
+     *
+     * @param inputStream the input stream
+     * @param encoding the encoding
+     * @return the output string
+     * @throws IOException if there is a problem reading from the stream
+     */
+    private static String readFully(final InputStream inputStream, final String encoding) throws IOException {
+        return new String(readFully(inputStream), encoding);
+    }
+
+    /**
+     * The current cache statuses mapped by <code>File</code> object.
+     */
+    private final Map<File, CacheStatus> cacheStatuses = new HashMap<File, CacheStatus>();
+
+    /**
+     * The maximum time to wait for all caching operations to complete.
+     */
+    private long maxWaitTime = DEFAULT_MAX_WAIT_TIME;
+
+    /**
+     * The time when the caching operation starts.
+     */
+    long start = 0;
+
+    /**
+     * Cache a file by submitting a 'jcache submit' process.
+     * <p>
+     * The resulting cache request will be registered with this manager until the {@link #clear()} method is called.
+     *
+     * @param file the file to cache which should be a path on the JLAB MSS (e.g. starts with '/mss')
+     */
+    private void cache(final File file) {
         if (!EvioFileUtilities.isMssFile(file)) {
             LOGGER.severe("file " + file.getPath() + " is not on the MSS");
             throw new IllegalArgumentException("Only files on the MSS can be cached.");
         }
+
+        if (EvioFileUtilities.getCachedFile(file).exists()) {
+            // Assume (maybe unreasonably?!) that since the file already exists it will stay in the cache for the duration of the job.
+            LOGGER.fine(file.getPath() + " is already on the cache disk so cache request is ignored");
+        } else {
+
+            // Execute the submit process.
+            final Process process = this.submit(file);
+
+            // Parse out the request ID from the process output.
+            final Integer requestId = this.getRequestId(process);
+
+            // Register the request with the manager.
+            final CacheStatus cacheStatus = new CacheStatus(file, requestId);
+            this.cacheStatuses.put(file, cacheStatus);
+
+            LOGGER.info("jcache submitted for " + file.getPath() + " with req ID '" + requestId + "'");
+        }
+    }
+
+    /**
+     * Submit cache request for every file in a list.
+     *
+     * @param files
+     */
+    void cache(final List<File> files) {
+        for (final File file : files) {
+            this.cache(file);
+        }
+    }
+
+    /**
+     * Return <code>true</code> if all files registered with the manager are cached.
+     *
+     * @return <code>true</code> if all files registered with the manager are cached
+     */
+    boolean checkCacheStatus() {
+
+        // Flag which will be changed to false if we find non-cached files in the loop.
+        boolean allCached = true;
+
+        // Loop over all cache statuses and refresh/check them.
+        for (final Entry<File, CacheStatus> entry : this.cacheStatuses.entrySet()) {
+
+            // Get the cache status for a single file.
+            final CacheStatus cacheStatus = entry.getValue();
+
+            LOGGER.info("checking status of " + cacheStatus.getFile().getPath() + " with req ID '" + cacheStatus.getRequestId() + "' ...");
+
+            // Is this file flagged as not non-cached?
+            if (!cacheStatus.isCached()) {
+
+                LOGGER.info("updating status of " + cacheStatus.getFile().getPath() + " ...");
+
+                // Update the cache status to see if it changed since last check.
+                cacheStatus.update();
+
+                // Is status still non-cached after status update?
+                if (!cacheStatus.isCached()) {
+
+                    // Set flag which indicates at least one file is not cached yet.
+                    allCached = false;
+
+                    LOGGER.info(entry.getKey() + " is NOT cached with status " + cacheStatus.getStatus(false));
+                } else {
+                    // Log that this file is now cached. It will not be checked next time.
+                    LOGGER.info(cacheStatus.getFile().getPath() + " is cached with status " + cacheStatus.getStatus(false));
+                }
+            } else {
+                LOGGER.info(cacheStatus.getFile().getPath() + " is already cached");
+            }
+        }
+        return allCached;
+    }
+
+    /**
+     * Clear all cache statuses.
+     */
+    void clear() {
+        this.cacheStatuses.clear();
+        this.start = 0;
+        LOGGER.info("CacheManager state was cleared.");
+    }
+
+    /**
+     * Get the request ID from a process that ran the 'jcache request' command.
+     *
+     * @param process the system process
+     * @return the request ID
+     */
+    private Integer getRequestId(final Process process) {
+        String output = null;
+        try {
+            output = readFully(process.getInputStream(), "US-ASCII");
+        } catch (final IOException e) {
+            throw new RuntimeException(e);
+        }
+        return Integer.parseInt(output.substring(output.indexOf("'") + 1, output.lastIndexOf("'")));
+    }
+
+    /**
+     * Get the number of files that are not cached.
+     *
+     * @return the number of files that are not cached
+     */
+    int getUncachedCount() {
+        int nUncached = 0;
+        for (final Entry<File, CacheStatus> entry : this.cacheStatuses.entrySet()) {
+            if (!entry.getValue().isCached()) {
+                nUncached += 1;
+            }
+        }
+        return nUncached;
+    }
+
+    /**
+     * Set the maximum wait time for caching to complete.
+     *
+     * @param maxWaitTime the maximum wait time for caching to complete
+     */
+    void setWaitTime(final long maxWaitTime) {
+        this.maxWaitTime = maxWaitTime;
+        LOGGER.config("max wait time set to " + maxWaitTime + " ms");
+    }
+
+    /**
+     * Sleep after checking cache statuses.
+     *
+     * @return <code>true</code> if <code>maxWaitTime</code> is exceeded or method is interrupted
+     */
+    private boolean sleep() {
+        final long elapsed = System.currentTimeMillis() - this.start;
+        LOGGER.info("elapsed time is " + elapsed + " ms");
+        if (elapsed > this.maxWaitTime) {
+            LOGGER.warning("max wait time of " + this.maxWaitTime + " ms was exceeded while caching files");
+            return true;
+        }
+        final Object lock = new Object();
+        synchronized (lock) {
+            try {
+                LOGGER.info("waiting " + POLL_WAIT_TIME + " ms before checking cache again ...");
+                lock.wait(POLL_WAIT_TIME);
+            } catch (final InterruptedException e) {
+                e.printStackTrace();
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Submit a cache request for a file.
+     *
+     * @param file the file
+     * @return the system process for the cache request command
+     */
+    private Process submit(final File file) {
         Process process = null;
         try {
             process = new ProcessBuilder("jcache", "submit", "default", file.getPath()).start();
@@ -133,88 +444,62 @@
         int status = 0;
         try {
             status = process.waitFor();
-        } catch (InterruptedException e) {
+        } catch (final InterruptedException e) {
             throw new RuntimeException("Process was interrupted.", e);
         }
         if (status != 0) {
-            throw new RuntimeException("The jcache process returned a non-zero exit status: " + status);
-        }
-        String output = null;
-        try {
-            output = readFully(process.getInputStream(), "US-ASCII");
-        } catch (IOException e) {
-            throw new RuntimeException(e);
-        }
-        Integer requestId = Integer.parseInt(output.substring(output.indexOf("'") + 1, output.lastIndexOf("'")));
-        FileInfo fileInfo = new FileInfo(file, requestId);
-        fileInfos.put(file, fileInfo);
-        LOGGER.info("jcache submitted for " + file.getPath() + " with req ID '" + requestId + "'");
-    }
-
-    private static Document buildDocument(String xmlString) {
-        SAXBuilder builder = new SAXBuilder();
-        Document document = null;
-        try {
-            builder.build(new InputSource(new StringReader(xmlString)));
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-        return document;
-    }
-
-    boolean waitForAll() {
-        if (this.fileInfos.isEmpty()) {
-            throw new IllegalStateException("There are no files being cached.");
-        }
+            throw new RuntimeException("The jcache process returned an error status of " + status);
+        }
+        return process;
+    }
+
+    /**
+     * Wait for all files registered with the manager to be cached or until a timeout occurs.
+     *
+     * @return <code>true</code> if all files are successfully cached
+     */
+    boolean waitForCache() {
+
         LOGGER.info("waiting for files to be cached ...");
-        long elapsed = 0;
+
+        if (this.cacheStatuses.isEmpty()) {
+            throw new IllegalStateException("There are no files registered with the cache manager.");
+        }
+
+        // This is the return value which will be changed to true if all files are cached successfully.
         boolean cached = false;
+
+        // Get the start time so we can calculate later if max wait time is exceeded.
+        this.start = System.currentTimeMillis();
+
+        // Keep checking files until they are all cached or the max wait time is exceeded.
         while (!cached) {
-            boolean check = true;    
-            INFO_LOOP: for (Entry<File, FileInfo> entry : fileInfos.entrySet()) {
-                FileInfo info = entry.getValue();
-                info.update();
-                if (!info.isCached()) {
-                    LOGGER.info(entry.getKey() + " is not cached with status " + info.getStatus());
-                    check = false;
-                    break INFO_LOOP;
-                }
-            }
-            if (check) {
+
+            // Check cache status of all files. This will return true if all files are cached.
+            final boolean allCached = this.checkCacheStatus();
+
+            // If all cache requests have succeeded then break from loop and set cache status to true.
+            if (allCached) {
                 cached = true;
                 break;
-            }
-            
-            elapsed = System.currentTimeMillis();
-            LOGGER.info("elapsed time: " + elapsed + " ms");
-            if (elapsed > maxWaitTime) {
+            } else {
+                LOGGER.info(this.getUncachedCount() + " files still uncached");
+            }
+
+            // Sleep for awhile before checking the cache statuses again.
+            // This will return true if max wait time is exceeded and the wait should be stopped.
+            final boolean waitTimeExceeded = this.sleep();
+
+            // Break from loop if the max wait time was exceeded.
+            if (waitTimeExceeded) {
                 break;
             }
-            Object lock = new Object();
-            synchronized(lock) {
-                try {
-                    LOGGER.info("waiting " + POLL_WAIT_TIME + " ms before checking again ...");
-                    lock.wait(POLL_WAIT_TIME);
-                } catch (InterruptedException e) {
-                    e.printStackTrace();
-                }
-            }
-        }
-        LOGGER.info("files were cached: " + cached);
+        }
+        if (cached) {
+            LOGGER.info("all files cached successfully!");
+        } else {
+            LOGGER.warning("failed to cache all files!");
+        }
         return cached;
     }
-
-    private static String readFully(InputStream inputStream, String encoding) throws IOException {
-        return new String(readFully(inputStream), encoding);
-    }
-
-    private static byte[] readFully(InputStream inputStream) throws IOException {
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        byte[] buffer = new byte[1024];
-        int length = 0;
-        while ((length = inputStream.read(buffer)) != -1) {
-            baos.write(buffer, 0, length);
-        }
-        return baos.toByteArray();
-    }
 }

Modified: java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/RunFilter.java
 =============================================================================
--- java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/RunFilter.java	(original)
+++ java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/RunFilter.java	Wed May 20 14:39:52 2015
@@ -4,13 +4,36 @@
 import java.io.FileFilter;
 import java.util.Set;
 
+/**
+ * A filter which rejects files that have a run number not in the accept list.
+ *
+ * @author Jeremy McCormick
+ */
 final class RunFilter implements FileFilter {
-    Set<Integer> acceptRuns;
 
+    /**
+     * Set of run numbers to accept.
+     */
+    private final Set<Integer> acceptRuns;
+
+    /**
+     * Create a new <code>RunFilter</code> with a set of runs to accept.
+     *
+     * @param acceptRuns the set of runs to accept
+     */
     RunFilter(final Set<Integer> acceptRuns) {
+        if (acceptRuns.isEmpty()) {
+            throw new IllegalArgumentException("the acceptRuns collection is empty");
+        }
         this.acceptRuns = acceptRuns;
     }
 
+    /**
+     * Returns <code>true</code> if file is accepted (its run number is in the set).
+     *
+     * @param file the EVIO file
+     * @return <code>true</code> if file is accepted
+     */
     @Override
     public boolean accept(final File file) {
         return this.acceptRuns.contains(EvioFileUtilities.getRunFromName(file));

Modified: java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/RunLog.java
 =============================================================================
--- java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/RunLog.java	(original)
+++ java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/RunLog.java	Wed May 20 14:39:52 2015
@@ -14,12 +14,34 @@
 import org.hps.conditions.database.ConnectionParameters;
 import org.lcsim.util.log.LogUtil;
 
+/**
+ * This class contains summary information about a series of runs that are themselves modeled with the {@link RunSummary} class. These can be looked
+ * up by their run number.
+ * <p>
+ * This class is able to update the run database using the <code>insert</code> methods.
+ *
+ * @author Jeremy McCormick
+ */
 final class RunLog {
 
+    /**
+     * Setup logging.
+     */
     private static final Logger LOGGER = LogUtil.create(RunLog.class);
 
-    Map<Integer, RunSummary> runs = new HashMap<Integer, RunSummary>();
+    /**
+     * A map between run numbers and the run summary information.
+     */
+    private final Map<Integer, RunSummary> runs = new HashMap<Integer, RunSummary>();
 
+    /**
+     * Get a run summary by run number.
+     * <p>
+     * It will be created if it does not exist.
+     *
+     * @param run the run number
+     * @return the <code>RunSummary</code> for the run number
+     */
     public RunSummary getRunSummary(final int run) {
         if (!this.runs.containsKey(run)) {
             LOGGER.info("creating new RunSummary for run " + run);
@@ -28,12 +50,22 @@
         return this.runs.get(run);
     }
 
+    /**
+     * Get a list of sorted run numbers in this run log.
+     * <p>
+     * This is a copy of the keys from the map so modifying it will have no effect on this class.
+     *
+     * @return the list of sorted run numbers
+     */
     List<Integer> getSortedRunNumbers() {
         final List<Integer> runList = new ArrayList<Integer>(this.runs.keySet());
         Collections.sort(runList);
         return runList;
     }
 
+    /**
+     * Insert all the information from the run log into the run database.
+     */
     void insert() {
 
         LOGGER.info("inserting runs into run_log ...");
@@ -42,9 +74,9 @@
         try {
             connection.setAutoCommit(false);
 
-            insertRunLog(connection);
+            this.insertRunLog(connection);
 
-            insertFiles(connection);
+            this.insertFiles(connection);
 
             connection.commit();
 
@@ -67,24 +99,36 @@
         }
     }
 
-    void insertFiles(final Connection connection) throws SQLException {
-        for (final int run : getSortedRunNumbers()) {
-            getRunSummary(run).getFiles().insert(connection, run);
+    /**
+     * Insert the file lists into the run database.
+     *
+     * @param connection the database connection
+     * @throws SQLException if there is an error executing the SQL query
+     */
+    private void insertFiles(final Connection connection) throws SQLException {
+        for (final int run : this.getSortedRunNumbers()) {
+            this.getRunSummary(run).getEvioFileList().insert(connection, run);
         }
     }
 
-    void insertRunLog(final Connection connection) throws SQLException {
+    /**
+     * Insert the run summary information into the database.
+     *
+     * @param connection the database connection
+     * @throws SQLException if there is an error querying the database
+     */
+    private void insertRunLog(final Connection connection) throws SQLException {
         PreparedStatement runLogStatement = null;
         runLogStatement = connection
                 .prepareStatement("INSERT INTO run_log (run, start_date, end_date, nevents, nfiles, end_ok, last_updated) VALUES(?, ?, ?, ?, ?, ?, NOW())");
-        for (final Integer run : getSortedRunNumbers()) {
+        for (final Integer run : this.getSortedRunNumbers()) {
             LOGGER.info("preparing to insert run " + run + " into database ..");
             final RunSummary runSummary = this.runs.get(run);
             runLogStatement.setInt(1, run);
             runLogStatement.setTimestamp(2, new java.sql.Timestamp(runSummary.getStartDate().getTime()));
             runLogStatement.setTimestamp(3, new java.sql.Timestamp(runSummary.getEndDate().getTime()));
             runLogStatement.setInt(4, runSummary.getTotalEvents());
-            runLogStatement.setInt(5, runSummary.getFiles().size());
+            runLogStatement.setInt(5, runSummary.getEvioFileList().size());
             runLogStatement.setBoolean(6, runSummary.isEndOkay());
             runLogStatement.executeUpdate();
             LOGGER.info("committed run " + run + " to run_log");
@@ -92,12 +136,18 @@
         LOGGER.info("run_log was updated!");
     }
 
+    /**
+     * Print out the run summaries to <code>System.out</code>.
+     */
     void printRunSummaries() {
         for (final int run : this.runs.keySet()) {
             this.runs.get(run).printRunSummary(System.out);
         }
     }
 
+    /**
+     * Sort all the file lists in place (by sequence number).
+     */
     void sortAllFiles() {
         for (final Integer run : this.runs.keySet()) {
             this.runs.get(run).sortFiles();

Modified: java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/RunProcessor.java
 =============================================================================
--- java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/RunProcessor.java	(original)
+++ java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/RunProcessor.java	Wed May 20 14:39:52 2015
@@ -3,77 +3,286 @@
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
+import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import org.hps.record.evio.EvioEventConstants;
 import org.hps.record.evio.EvioEventProcessor;
 import org.jlab.coda.jevio.EvioEvent;
 import org.jlab.coda.jevio.EvioException;
 import org.jlab.coda.jevio.EvioReader;
+import org.lcsim.util.log.DefaultLogFormatter;
 import org.lcsim.util.log.LogUtil;
 
-public final class RunProcessor {
-
-    private static final Logger LOGGER = LogUtil.create(RunProcessor.class);
-
-    List<EvioEventProcessor> processors = new ArrayList<EvioEventProcessor>();
-
-    RunSummary runSummary;
-
-    RunProcessor(final RunSummary runSummary) {
+/**
+ * Processes all the EVIO files from a run.
+ * <p>
+ * This class is a wrapper for activating different sub-tasks, including optionally caching all files from the JLAB MSS to the cache disk using
+ * jcache.
+ * <p>
+ * There is also a list of processors which is run on all events from the run, if the processor list is not empty.
+ *
+ * @author Jeremy McCormick
+ */
+final class RunProcessor {
+
+    /**
+     * Setup logger.
+     */
+    private static final Logger LOGGER = LogUtil.create(RunProcessor.class, new DefaultLogFormatter(), Level.FINE);
+
+    /**
+     * The cache manager.
+     */
+    private final JCacheManager cacheManager;
+
+    /**
+     * The event printing interval when processing EVIO files.
+     */
+    private int eventPrintInterval = 1000;
+
+    /**
+     * Max files to read (defaults to unlimited).
+     */
+    private int maxFiles = -1;
+
+    /**
+     * The list of EVIO processors to run on the files that are found.
+     */
+    private final List<EvioEventProcessor> processors = new ArrayList<EvioEventProcessor>();
+
+    /**
+     * The run summary information updated by running this processor.
+     */
+    private final RunSummary runSummary;
+
+    /**
+     * Set to <code>true</code> to use file caching.
+     */
+    private boolean useFileCache;
+
+    /**
+     * Create a new run processor.
+     *
+     * @param runSummary the run summary to update
+     * @param cacheManager the cache manager for executing 'jcache' commands
+     */
+    RunProcessor(final RunSummary runSummary, final JCacheManager cacheManager) {
         this.runSummary = runSummary;
-    }
-
+        this.cacheManager = cacheManager;
+    }
+
+    /**
+     * Add a processor of EVIO events.
+     *
+     * @param processor the EVIO event processor
+     */
     void addProcessor(final EvioEventProcessor processor) {
         this.processors.add(processor);
-        LOGGER.config("added processor: " + processor.getClass().getSimpleName());
-    }
-
+        LOGGER.config("added processor " + processor.getClass().getSimpleName());
+    }
+
+    /**
+     * Cache all files and wait for the operation to complete.
+     * <p>
+     * Potentially, this operation can take a very long time. This can be managed using the {@link JCacheManager#setWaitTime(long)} method to set a
+     * timeout.
+     */
+    private void cacheFiles() {
+
+        LOGGER.info("caching files from run " + this.runSummary.getRun() + " ...");
+
+        // Cache all the files and wait for the operation to complete (it will take awhile!).
+        this.cacheManager.cache(this.getFiles());
+        final boolean cached = this.cacheManager.waitForCache();
+
+        // If the files weren't cached then die.
+        if (!cached) {
+            throw new RuntimeException("The cache process did not complete in time.");
+        }
+
+        LOGGER.info("done caching files from run " + this.runSummary.getRun());
+    }
+
+    Integer computeEventCount(final EvioReader reader) throws IOException, EvioException {
+        return reader.getEventCount();
+    }
+
+    /**
+     * Get the list of files to process, which will be limited by the {@link #maxFiles} value if it is set.
+     *
+     * @return the files to process
+     */
+    private List<File> getFiles() {
+        // Get the list of files to process, taking into account the max files setting.
+        List<File> files = this.runSummary.getEvioFileList();
+        if (this.maxFiles != -1) {
+            LOGGER.info("limiting processing to first " + this.maxFiles + " files from max files setting");
+            files = files.subList(0, this.maxFiles - 1);
+        }
+        return files;
+    }
+
+    /**
+     * Get the list of EVIO processors.
+     *
+     * @return the list of EVIO processors
+     */
     List<EvioEventProcessor> getProcessors() {
         return this.processors;
     }
 
+    boolean isEndOkay(final EvioReader reader) throws Exception {
+        LOGGER.info("checking is END okay ...");
+        boolean endOkay = false;
+        reader.gotoEventNumber(reader.getEventCount() - 2);
+        EvioEvent event = null;
+        while ((event = reader.parseNextEvent()) != null) {
+            if (event.getHeader().getTag() == EvioEventConstants.END_EVENT_TAG) {
+                endOkay = true;
+                break;
+            }
+        }
+        return endOkay;
+    }
+
+    /**
+     * Process the run.
+     *
+     * @throws Exception if there is an error processing a file
+     */
     void process() throws Exception {
-        
+
+        LOGGER.info("processing run " + this.runSummary.getRun() + " ...");
+
+        // First cache all the files we will process, if necessary.
+        if (this.useFileCache) {
+            this.cacheFiles();
+        }
+
         // Run the start of job hooks.
         for (final EvioEventProcessor processor : this.processors) {
             processor.startJob();
         }
-         
-        
-        
-        // Process the files.
-        for (final File file : this.runSummary.getFiles()) {
-            process(file);
-        }
-        
+
+        // Process all the files.
+        for (final File file : this.getFiles()) {
+            this.process(file);
+        }
+
         // Run the end of job hooks.
         for (final EvioEventProcessor processor : this.processors) {
             processor.endJob();
         }
-    }
-
+
+        LOGGER.info("done processing run " + this.runSummary.getRun());
+    }
+
+    /**
+     * Process a single EVIO file from the run.
+     *
+     * @param file the EVIO file
+     * @throws EvioException if there is an EVIO error
+     * @throws IOException if there is some kind of IO error
+     * @throws Exception if there is a generic error thrown by event processing
+     */
+    // FIXME: I think this method is terribly inefficient right now.
     private void process(final File file) throws EvioException, IOException, Exception {
+        LOGGER.fine("processing " + file.getPath() + " ...");
+
         EvioReader reader = null;
         try {
             // Open with wrapper method which will use the cached file path if necessary.
+            LOGGER.fine("opening " + file.getPath() + " for reading ...");
             reader = EvioFileUtilities.open(file);
-            
-            // Compute event count for the file and store the value.
-            this.runSummary.getFiles().computeEventCount(reader, file);
-            
+            LOGGER.fine("done opening " + file.getPath());
+
+            // If this is the first file then get the start date.
+            if (file.equals(this.runSummary.getEvioFileList().first())) {
+                LOGGER.fine("getting run start ...");
+                final Date runStart = EvioFileUtilities.getRunStart(file);
+                LOGGER.fine("got run start " + runStart);
+                this.runSummary.setStartDate(runStart);
+            }
+
+            // Compute event count for the file and store the value in the run summary's file list.
+            LOGGER.info("getting event count for " + file.getPath() + "...");
+            final int eventCount = this.computeEventCount(reader);
+            this.runSummary.getEvioFileList().setEventCount(file, eventCount);
+            LOGGER.info("set event count " + eventCount + " for " + file.getPath());
+
             // Process the events using the list of EVIO processors.
-            EvioEvent event = null;
-            while ((event = reader.parseNextEvent()) != null) {
-                for (final EvioEventProcessor processor : this.processors) {
-                    processor.process(event);
+            LOGGER.info("running EVIO processors ...");
+            reader.gotoEventNumber(0);
+            int nProcessed = 0;
+            if (!this.processors.isEmpty()) {
+                EvioEvent event = null;
+                while ((event = reader.parseNextEvent()) != null) {
+                    for (final EvioEventProcessor processor : this.processors) {
+                        processor.process(event);
+                        ++nProcessed;
+                        if (nProcessed % this.eventPrintInterval == 0) {
+                            LOGGER.finer("processed " + nProcessed + " EVIO events");
+                        }
+                    }
                 }
             }
+            LOGGER.info("done running EVIO processors");
+
+            // Check if END event is present if this is the last file in the run.
+            if (file.equals(this.runSummary.getEvioFileList().last())) {
+                LOGGER.info("checking end okay ...");
+                final boolean endOkay = this.isEndOkay(reader);
+                this.runSummary.setEndOkay(endOkay);
+                LOGGER.info("endOkay set to " + endOkay);
+
+                LOGGER.info("getting end date ...");
+                final Date endDate = EvioFileUtilities.getRunEnd(file);
+                this.runSummary.setEndDate(endDate);
+                LOGGER.info("found end date " + endDate);
+            }
+
         } finally {
             if (reader != null) {
                 reader.close();
             }
         }
-    }
-   
+        LOGGER.fine("done processing " + file.getPath());
+    }
+
+    /**
+     * Set the event print interval when running the EVIO processors.
+     *
+     * @param eventPrintInterval the event print interval when running the EVIO processors
+     */
+    void setEventPrintInterval(final int eventPrintInterval) {
+        this.eventPrintInterval = eventPrintInterval;
+    }
+
+    /**
+     * Set the maximum number of files to process.
+     * <p>
+     * This is primarily used for debugging purposes.
+     *
+     * @param maxFiles the maximum number of files to process
+     */
+    void setMaxFiles(final int maxFiles) {
+        this.maxFiles = maxFiles;
+        LOGGER.config("max files set to " + maxFiles);
+    }
+
+    /**
+     * Set whether or not to use the file caching, which copies files from the JLAB MSS to the cache disk.
+     * <p>
+     * Since EVIO data files at JLAB are primarily kept on the MSS, running without this option enabled there will likely cause the job to fail.
+     *
+     * @param cacheFiles <code>true</code> to enabled file caching
+     */
+    void useFileCache(final boolean cacheFiles) {
+        this.useFileCache = cacheFiles;
+        LOGGER.config("file caching enabled");
+    }
+
 }

Modified: java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/RunSummary.java
 =============================================================================
--- java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/RunSummary.java	(original)
+++ java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/RunSummary.java	Wed May 20 14:39:52 2015
@@ -1,17 +1,12 @@
 package org.hps.record.evio.crawler;
 
 import java.io.File;
-import java.io.IOException;
 import java.io.PrintStream;
 import java.util.Date;
 import java.util.Map;
 import java.util.logging.Logger;
 
 import org.hps.record.epics.EpicsData;
-import org.hps.record.evio.EvioEventConstants;
-import org.jlab.coda.jevio.EvioEvent;
-import org.jlab.coda.jevio.EvioException;
-import org.jlab.coda.jevio.EvioReader;
 import org.lcsim.util.log.LogUtil;
 
 /**
@@ -31,21 +26,65 @@
  */
 final class RunSummary {
 
+    /**
+     * Setup logger.
+     */
     private static final Logger LOGGER = LogUtil.create(RunSummary.class);
 
+    /**
+     * The end date of the run.
+     */
     private Date endDate;
+
+    /**
+     * This is <code>true</code> if the END event is found in the data.
+     */
+    private boolean endOkay;
+
+    /**
+     * The combined EPICS information for the run (uses the mean values for each variable).
+     */
     private EpicsData epics;
+
+    /**
+     * The counts of different types of events that were found.
+     */
     private Map<Object, Integer> eventTypeCounts;
+
+    /**
+     * The list of EVIO files in the run.
+     */
     private final EvioFileList files = new EvioFileList();
-    private Boolean isEndOkay;
+
+    /**
+     * The run number.
+     */
     private final int run;
+
+    /**
+     * The start date of the run.
+     */
     private Date startDate;
+
+    /**
+     * The total events found in the run across all files.
+     */
     private int totalEvents = -1;
 
+    /**
+     * Create a run summary.
+     *
+     * @param run the run number
+     */
     RunSummary(final int run) {
         this.run = run;
     }
 
+    /**
+     * Add an EVIO file from this run to the list.
+     *
+     * @param file the file to add
+     */
     void addFile(final File file) {
         this.files.add(file);
 
@@ -53,77 +92,97 @@
         this.totalEvents = -1;
     }
 
+    /**
+     * Get the date when the run ended.
+     * <p>
+     * This will be extracted from the EVIO END event. If there is no END record it will be the last event time.
+     *
+     * @return the date when the run ended
+     */
     Date getEndDate() {
-        if (this.endDate == null) {
-            this.endDate = EvioFileUtilities.getRunEnd(this.files.last());
-        }
         return this.endDate;
     }
 
+    /**
+     * Get the EPICS data summary.
+     * <p>
+     * This is computed by taking the mean of each variable for the run.
+     *
+     * @return the EPICS data summary
+     */
     EpicsData getEpicsData() {
         return this.epics;
     }
 
+    /**
+     * Get the counts of different event types.
+     *
+     * @return the counts of different event types
+     */
     Map<Object, Integer> getEventTypeCounts() {
         return this.eventTypeCounts;
     }
 
-    EvioFileList getFiles() {
+    /**
+     * Get the list of EVIO files in this run.
+     *
+     * @return the list of EVIO files in this run
+     */
+    EvioFileList getEvioFileList() {
         return this.files;
     }
 
+    /**
+     * Get the run number.
+     *
+     * @return the run number
+     */
+    int getRun() {
+        return this.run;
+    }
+
+    /**
+     * Get the start date of the run.
+     *
+     * @return the start date of the run
+     */
     Date getStartDate() {
-        if (this.startDate == null) {
-            this.startDate = EvioFileUtilities.getRunStart(this.files.first());
-        }
         return this.startDate;
     }
 
+    /**
+     * Get the total events in the run.
+     *
+     * @return the total events in the run
+     */
     int getTotalEvents() {
         if (this.totalEvents == -1) {
-            this.totalEvents = this.files.computeTotalEvents();
+            this.totalEvents = this.files.getTotalEvents();
         }
         return this.totalEvents;
     }
 
+    /**
+     * Return <code>true</code> if END event was found in the data.
+     *
+     * @return <code>true</code> if END event was in the data
+     */
     boolean isEndOkay() {
-        if (this.isEndOkay == null) {
-            LOGGER.info("checking is END okay ...");
-            this.isEndOkay = false;
-            final File lastFile = this.files.last();
-            EvioReader reader = null;
-            try {
-                reader = EvioFileUtilities.open(lastFile);
-                reader.gotoEventNumber(reader.getEventCount() - 5);
-                EvioEvent event = null;
-                while ((event = reader.parseNextEvent()) != null) {
-                    if (event.getHeader().getTag() == EvioEventConstants.END_EVENT_TAG) {
-                        this.isEndOkay = true;
-                        break;
-                    }
-                }
-            } catch (EvioException | IOException e) {
-                throw new RuntimeException(e);
-            } finally {
-                if (reader != null) {
-                    try {
-                        reader.close();
-                    } catch (final IOException e) {
-                        e.printStackTrace();
-                    }
-                }
-            }
-        }
-        return this.isEndOkay;
-    }
-
+        return this.endOkay;
+    }
+
+    /**
+     * Print the run summary.
+     *
+     * @param ps the print stream for output
+     */
     void printRunSummary(final PrintStream ps) {
         ps.println("--------------------------------------------");
         ps.println("run: " + this.run);
         ps.println("first file: " + this.files.first());
         ps.println("last file: " + this.files.last());
-        ps.println("started: " + getStartDate());
-        ps.println("ended: " + getEndDate());
+        ps.println("started: " + this.getStartDate());
+        ps.println("ended: " + this.getEndDate());
         ps.println("total events: " + this.getTotalEvents());
         ps.println("event types");
         for (final Object key : this.eventTypeCounts.keySet()) {
@@ -135,14 +194,54 @@
         }
     }
 
+    /**
+     * Set the end date.
+     *
+     * @param endDate the end date
+     */
+    void setEndDate(final Date endDate) {
+        this.endDate = endDate;
+    }
+
+    /**
+     * Set if end is okay.
+     *
+     * @param endOkay <code>true</code> if end is okay
+     */
+    void setEndOkay(final boolean endOkay) {
+        this.endOkay = endOkay;
+    }
+
+    /**
+     * Set the EPICS data for the run.
+     *
+     * @param epics the EPICS data for the run
+     */
     void setEpicsData(final EpicsData epics) {
         this.epics = epics;
     }
 
+    /**
+     * Set the event type counts for the run.
+     *
+     * @param eventTypeCounts the event type counts for the run
+     */
     void setEventTypeCounts(final Map<Object, Integer> eventTypeCounts) {
         this.eventTypeCounts = eventTypeCounts;
     }
 
+    /**
+     * Set the start date of the run.
+     *
+     * @param startDate the start date of the run
+     */
+    void setStartDate(final Date startDate) {
+        this.startDate = startDate;
+    }
+
+    /**
+     * Sort the files in the run by sequence number in place.
+     */
     void sortFiles() {
         this.files.sort();
     }

Added: java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/package-info.java
 =============================================================================
--- java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/package-info.java	(added)
+++ java/trunk/record-util/src/main/java/org/hps/record/evio/crawler/package-info.java	Wed May 20 14:39:52 2015
@@ -0,0 +1,6 @@
+/**
+ * Implements an EVIO file crawler for extracting run and configuration information, including run start and end dates, event counts, etc.
+ *
+ * @author Jeremy McCormick
+ */
+package org.hps.record.evio.crawler;