edu.hawaii.soest.kilonalu.utilities.FileArchiverSink.java Source code

Java tutorial

Introduction

Here is the source code for edu.hawaii.soest.kilonalu.utilities.FileArchiverSink.java

Source

/*
 * RDV
 * Real-time Data Viewer
 * http://it.nees.org/software/rdv/
 * 
 * Copyright (c) 2005-2007 University at Buffalo
 * Copyright (c) 2005-2007 NEES Cyberinfrastructure Center
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 * 
 * $URL$
 * $Revision$
 * $Date$
 * $Author$
 */
package edu.hawaii.soest.kilonalu.utilities;

import java.io.File;
import java.io.IOException;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Calendar;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.log4j.Logger;
import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.PropertyConfigurator;

import org.nees.rbnb.MarkerUtilities;
import org.nees.rbnb.RBNBBase;
import org.nees.rbnb.RBNBUtilities;
import org.nees.rbnb.TimeProgressListener;
import org.nees.rbnb.TimeRange;

import com.rbnb.sapi.ChannelMap;
import com.rbnb.sapi.SAPIException;
import com.rbnb.sapi.Sink;
import com.rbnb.sapi.ChannelTree.Node;

/**
 * This class grabs data from an RBNB data source and saves it to a
 * directory structure where the data for the time stamp
 * yyyy-MM-dd:hh:mm:ss.nnn is saved to the file prefix_yyyyMMddhhmmssnnn.dat on the
 * directory path base-dir/yyyy/MM/dd/[hh/mm/]. The spliting of files to directory
 * structures is done to assure that no directory overflows its index table.
 * 
 * @author Terry E. Weymouth
 * @author Moji Soltani
 * @author Jason P. Hanley
 * @author Christopher Jones
 */
public class FileArchiverSink extends RBNBBase {

    /** The Logger instance used to log system messages */
    private static Logger logger = Logger.getLogger(FileArchiverSink.class);

    /** the default RBNB sink name */
    private static final String DEFAULT_SINK_NAME = "FileArchiver";

    /** the default RBNB source name */
    private static final String DEFAULT_SOURCE_NAME = "KN0101_010ADCP010R00";

    /** the default RBNB channel name */
    private static final String DEFAULT_CHANNEL_NAME = "BinaryPD0EnsembleData";

    /** the default File prefix for archived filenames */
    private static final String DEFAULT_FILE_PREFIX = "KNXXXX_XXXADCPXXXRXX_";

    /** the File prefix for archived filenames */
    private String filePrefix = DEFAULT_FILE_PREFIX;

    /** the default File extension for archived filenames */
    private static final String DEFAULT_FILE_EXTENSION = ".10.1.dat";

    /** the File extension for archived filenames */
    private String fileExtension = DEFAULT_FILE_EXTENSION;

    /**
     * The archive interval used to periodically archive data (in seconds)
     */
    private int archiveInterval;

    /** 
     * the default File path depth archived file directory paths.  The should
     * be one a SimpleDateFormat object of yyyy, MM, dd, HH, or mm.  It determines
     * if files are archived in directories down to Year, Month, Day, Hour, 
     * or Minute.  
     */
    private static final SimpleDateFormat DEFAULT_FILE_PATH_DEPTH = new SimpleDateFormat("dd");

    private SimpleDateFormat filePathDepth = DEFAULT_FILE_PATH_DEPTH;
    /** the RBNB sink */
    private final Sink sink;

    /** the RBNB sink name */
    private String sinkName = DEFAULT_SINK_NAME;

    /** the RBNB source name */
    private String sourceName = DEFAULT_SOURCE_NAME;

    /** the RBNB channel name */
    private String channelName = DEFAULT_CHANNEL_NAME;

    /** the full RBNB channel path */
    private String channelPath = sourceName + "/" + channelName;

    /** the start time for data export */
    private double startTime = 0.0;

    /** the end time for data export */
    private double endTime = Double.MAX_VALUE;

    /** the Calendar representation of the FileArchiver start time **/
    private static Calendar endArchiveCal;

    /** the Calendar representation of the FileArchiver start time **/
    private static Calendar beginArchiveCal;

    /** the event marker filter string */
    private String eventMarkerFilter;

    /** the list of time ranges to export */
    private List<TimeRange> timeRanges;

    /** the default directory to archive to */
    public static final File DEFAULT_ARCHIVE_DIRECTORY = new File("/data/rbnb");

    /** the directory to archive to */
    private File archiveDirectory = DEFAULT_ARCHIVE_DIRECTORY;

    /** a flag to indicate if we are connected to the RBNB server or not */
    private boolean connected = false;

    /** a flag to control the export process */
    private boolean doExport = false;

    /** a list of registered progress listeners */
    private List<TimeProgressListener> listeners = new ArrayList<TimeProgressListener>();

    /** the duration of all the data to be exported */
    private double duration;

    /** number of seconds to go back from now to set a start time */
    private int secondsResetStart;

    /**
     * Constructor: creates FileArchiverSink.
     */
    public FileArchiverSink() {
        super();
        sink = new Sink();
    }

    /**
     * Runs FileArchiverSink.
     * 
     * @param args  the command line arguments
     */
    public static void main(String[] args) {

        // Set up a simple logger that logs to the console
        BasicConfigurator.configure();

        final FileArchiverSink fileArchiverSink = new FileArchiverSink();

        if (fileArchiverSink.parseArgs(args)) {

            setupShutdownHook(fileArchiverSink);
            setupProgressListener(fileArchiverSink);

            // archive data on a schedule
            if (fileArchiverSink.getArchiveInterval() > 0) {
                // override the command line start and end times      
                fileArchiverSink.setupArchiveTime(fileArchiverSink);

                TimerTask archiveData = new TimerTask() {
                    public void run() {
                        logger.debug("TimerTask.run() called.");

                        if (fileArchiverSink.validateSetup()) {
                            fileArchiverSink.export();
                            fileArchiverSink.setupArchiveTime(fileArchiverSink);
                        }
                    }
                };

                Timer archiveTimer = new Timer();
                // run the archiveData timer task on the hour, every hour (or every day)
                archiveTimer.scheduleAtFixedRate(archiveData, endArchiveCal.getTime(),
                        fileArchiverSink.getArchiveInterval() * 1000);

                // archive data once based on the start and end times  
            } else {
                fileArchiverSink.export();

            }
        }
    }

    /**
     * A method that initializes time variables for the File Archiver class.  For 
     * now, it overides the start and end times provided on the command line
     * and rolls the end time forward to be on the hour, and sets the 
     * start time to be one hour prior.  This results in hourly data files written
     * on the hour.
     */
    private void setupArchiveTime(final FileArchiverSink fileArchiverSink) {
        logger.debug("FileArchiverSink.setupArchiveTime() called.");

        // remove the time ranges assumed from the command line args
        timeRanges.clear();

        long eTime; // intermediate end time variable
        Date sDate; // intermediate start date variable
        long sTime; // intermediate start time variable

        if (getArchiveInterval() == 120) {

            // set the execution time to be every two minutes (debug mode)    
            endArchiveCal = Calendar.getInstance();
            endArchiveCal.clear(Calendar.MILLISECOND);
            endArchiveCal.clear(Calendar.SECOND);
            endArchiveCal.add(Calendar.MINUTE, 2);

            eTime = (endArchiveCal.getTime()).getTime();
            endTime = ((double) eTime) / 1000.0;

            /** the Calendar representation of the FileArchiver begin time **/
            beginArchiveCal = (Calendar) endArchiveCal.clone();

            // set the begin time of the duration 2 minutes prior to the execution time
            beginArchiveCal.add(Calendar.MINUTE, -2);
            endArchiveCal.add(Calendar.SECOND, -1);
            sDate = beginArchiveCal.getTime();
            sTime = sDate.getTime();
            startTime = ((double) sTime) / 1000.0;
            logger.debug("Next archive time will be " + endArchiveCal.getTime().toString());
            logger.debug("Archive begin time will be " + beginArchiveCal.getTime().toString());

            // schedule hourly on the hour
        } else if (getArchiveInterval() == 3600) {
            // set the execution time to be on the upcoming hour.  Add a minute to
            // now() to be sure the next interval is in the next hour   
            endArchiveCal = Calendar.getInstance();
            endArchiveCal.add(Calendar.MINUTE, 1);
            endArchiveCal.clear(Calendar.MILLISECOND);
            endArchiveCal.clear(Calendar.SECOND);
            endArchiveCal.clear(Calendar.MINUTE);
            endArchiveCal.add(Calendar.HOUR_OF_DAY, 1);

            eTime = (endArchiveCal.getTime()).getTime();
            endTime = ((double) eTime) / 1000.0;

            /** the Calendar representation of the FileArchiver begin time **/
            beginArchiveCal = (Calendar) endArchiveCal.clone();

            // set the begin time of the duration 1 hour prior to the execution time
            beginArchiveCal.add(Calendar.HOUR_OF_DAY, -1);
            endArchiveCal.add(Calendar.SECOND, -1);
            sDate = beginArchiveCal.getTime();
            sTime = sDate.getTime();
            startTime = ((double) sTime) / 1000.0;
            logger.debug("Next archive time will be " + endArchiveCal.getTime().toString());
            logger.debug("Archive begin time will be " + beginArchiveCal.getTime().toString());

            // else schedule daily on the day
        } else if (getArchiveInterval() == 86400) {
            // set the execution time to be on the upcoming day    
            endArchiveCal = Calendar.getInstance();
            endArchiveCal.add(Calendar.MINUTE, 1);
            endArchiveCal.clear(Calendar.MILLISECOND);
            endArchiveCal.clear(Calendar.SECOND);
            endArchiveCal.clear(Calendar.MINUTE);
            endArchiveCal.set(Calendar.HOUR_OF_DAY, 0);
            endArchiveCal.add(Calendar.DATE, 1);

            eTime = (endArchiveCal.getTime()).getTime();
            endTime = ((double) eTime) / 1000.0;

            /** the Calendar representation of the FileArchiver begin time **/
            beginArchiveCal = (Calendar) endArchiveCal.clone();

            // set the begin time of the duration 1 day prior to the execution time
            beginArchiveCal.add(Calendar.DATE, -1);
            endArchiveCal.add(Calendar.SECOND, -1);
            sDate = beginArchiveCal.getTime();
            sTime = sDate.getTime();
            startTime = ((double) sTime) / 1000.0;
            logger.debug("Next archive time will be " + endArchiveCal.getTime().toString());
            logger.debug("Archive begin time will be " + beginArchiveCal.getTime().toString());

            // else schedule weekly on the day
        } else if (getArchiveInterval() == 604800) {
            // set the execution time to be on the upcoming hour    
            endArchiveCal = Calendar.getInstance();
            endArchiveCal.add(Calendar.MINUTE, 1);
            endArchiveCal.clear(Calendar.MILLISECOND);
            endArchiveCal.clear(Calendar.SECOND);
            endArchiveCal.clear(Calendar.MINUTE);
            endArchiveCal.set(Calendar.HOUR_OF_DAY, 0);
            endArchiveCal.add(Calendar.DATE, 7);

            eTime = (endArchiveCal.getTime()).getTime();
            endTime = ((double) eTime) / 1000.0;

            /** the Calendar representation of the FileArchiver begin time **/
            beginArchiveCal = (Calendar) endArchiveCal.clone();

            // set the begin time of the duration 1 day prior to the execution time
            beginArchiveCal.add(Calendar.DATE, -7);
            endArchiveCal.add(Calendar.SECOND, -1);
            sDate = beginArchiveCal.getTime();
            sTime = sDate.getTime();
            startTime = ((double) sTime) / 1000.0;
            logger.debug("Next archive time will be " + endArchiveCal.getTime().toString());
            logger.debug("Archive begin time will be " + beginArchiveCal.getTime().toString());

        }
    }

    /**
     * Adds a shutdown hook to stop the export when called.
     * 
     * @param fileArchiverSink  the FileArchiverSink to stop
     */
    private static void setupShutdownHook(final FileArchiverSink fileArchiverSink) {
        logger.debug("FileArchiverSink.setupShutdownHook() called.");
        final Thread workerThread = Thread.currentThread();

        Runtime.getRuntime().addShutdownHook(new Thread() {
            public void run() {
                fileArchiverSink.stopExport();
                try {
                    workerThread.join();
                } catch (InterruptedException e) {
                }
            }
        });
    }

    /**
     * Adds a progress listener to printout the status of the export
     * 
     * @param fileArchiverSink  the FileArchiverSink to monitor
     */
    private static void setupProgressListener(FileArchiverSink fileArchiverSink) {
        logger.debug("FileArchiverSink.setupProgressListener() called.");
        fileArchiverSink.addTimeProgressListener(new TimeProgressListener() {
            public void progressUpdate(double estimatedDuration, double consumedTime) {
                if (estimatedDuration == Double.MAX_VALUE) {
                    logger.info("Exported " + Math.round(consumedTime) + " seconds of data...");
                } else {
                    logger.info("Export of data " + Math.round(100 * consumedTime / estimatedDuration)
                            + "% complete...");
                }
            }
        });
    }

    /**
     * This method overrides the setOptions() method in RBNBBase and adds in 
     * options for the various command line flags.
     */
    protected Options setOptions() {
        Options opt = setBaseOptions(new Options()); // uses h, s, p
        opt.addOption("k", true, "Sink Name (defaults to " + DEFAULT_SINK_NAME + ")");
        opt.addOption("n", true, "Source Name (defaults to " + DEFAULT_SOURCE_NAME + ")");
        opt.addOption("c", true, "Source Channel Name (defaults to " + DEFAULT_CHANNEL_NAME + ")");
        opt.addOption("d", true, "Base directory path (defaults to " + DEFAULT_ARCHIVE_DIRECTORY + ")");
        opt.addOption("S", true, "Start time (defauts to now)");
        opt.addOption("E", true, "End time (defaults to forever)");
        opt.addOption("I", true,
                "Interval (hourly, daily, or weekly) to periodically archive data\n Mututally exclusive with -E and -S");
        opt.addOption(OptionBuilder.withDescription("Event markers to filter start/stop times").hasOptionalArg()
                .create("M"));
        opt.addOption("B", true,
                "Number of seconds to go back from now to set start time\n Mututally exclusive with -E and -S");

        setNotes("Writes data frames between start time and end time to the "
                + "directory structure starting at the base directory. The time "
                + "format is yyyy-mm-dd:hhTmm:ss.nnn.");
        return opt;
    }

    /**
     * A method that sets the prefix string to be used in the archived file name
     * 
     * @param filePrefix  the prefix string to be used in the file name
     */
    public void setFilePrefix(String filePrefix) {
        this.filePrefix = filePrefix;
    }

    /**
     * A method that sets the extension string to be used in the archived file name
     * 
     * @param fileExtension  the extension string to be used in the file name
     */
    public void setFileExtension(String fileExtension) {
        this.fileExtension = fileExtension;
    }

    /**
     * This method overrides the setArgs() method in RBNBBase and sets the values
     * of the various command line arguments
     */
    protected boolean setArgs(CommandLine cmd) {
        logger.debug("FileArchiverSink.setArgs() called.");

        if (!setBaseArgs(cmd))
            return false;

        if (cmd.hasOption('n')) {
            String a = cmd.getOptionValue('n');
            if (a != null)
                sourceName = a;
            setFilePrefix(a + "_");
        }

        if (cmd.hasOption('c')) {
            String a = cmd.getOptionValue('c');
            if (a != null)
                channelName = a;
        }

        if (cmd.hasOption('k')) {
            String a = cmd.getOptionValue('k');
            if (a != null)
                sinkName = a;
        }

        if (cmd.hasOption('d')) {
            String a = cmd.getOptionValue('d');
            if (a != null)
                archiveDirectory = new File(a);
        }

        if (cmd.hasOption('S')) {
            String a = cmd.getOptionValue('S');
            if (a != null) {
                try {
                    Date d = FileArchiveUtility.getCommandFormat().parse(a);
                    long t = d.getTime();
                    startTime = ((double) t) / 1000.0;
                } catch (Exception e) {
                    logger.debug("Parse of start time failed " + a + e.getMessage());
                    printUsage();
                    return false;
                }
            }
        } else if (!cmd.hasOption('M')) {
            startTime = System.currentTimeMillis() / 1000d;
        }

        if (cmd.hasOption('E')) {
            String a = cmd.getOptionValue('E');
            if (a != null) {
                try {
                    Date d = FileArchiveUtility.getCommandFormat().parse(a);
                    long t = d.getTime();
                    endTime = ((double) t) / 1000.0;
                } catch (Exception e) {
                    logger.debug("Parse of end time failed " + a);
                    printUsage();
                    return false;
                }
            }
        }

        if (cmd.hasOption('B')) {
            String a = cmd.getOptionValue('B');
            if (a != null) {
                try {
                    secondsResetStart = Integer.parseInt(a);
                    startTime = System.currentTimeMillis() / 1000d - secondsResetStart;
                    endTime = System.currentTimeMillis() / 1000d;
                    endArchiveCal = Calendar.getInstance();

                } catch (NumberFormatException nf) {
                    logger.debug("Please enter a number for seconds to reset the start to.");
                    return false;
                }
            }
        }
        if (startTime >= endTime) {
            logger.debug("The start time must come before the end time.");
            return false;
        }

        if (cmd.hasOption('M')) {
            String a = cmd.getOptionValue('M');
            if (a != null) {
                eventMarkerFilter = a;
            } else {
                eventMarkerFilter = "";
            }
        }

        // handle the -I option, test if it's an allowed value
        if (cmd.hasOption("I")) {
            String interval = cmd.getOptionValue("I");
            if (interval != null) {
                try {
                    if (interval.equals("hourly")) {
                        setArchiveInterval(3600);

                    } else if (interval.equals("daily")) {
                        setArchiveInterval(86400);

                    } else if (interval.equals("weekly")) {
                        setArchiveInterval(604800);

                    } else if (interval.equals("debug")) {
                        setArchiveInterval(120);

                    } else {
                        logger.debug("Please enter either hourly, daily, or weekly for the archiving interval.");

                    }
                } catch (NumberFormatException nf) {
                    return false;
                }
            }
        }

        channelPath = sourceName + "/" + channelName;

        return validateSetup();
    }

    /**
     * Setup the paramters for data export.
     * 
     * @param serverName         the RBNB server name
     * @param serverPort         the RBNB server port
     * @param sinkName           the RBNB sink name
     * @param channelPath        the full channel path
     * @param archiveDirectory   the directory to archive to
     * @param startTime          the start time
     * @param endTime            the end time
     * @param eventMarkerFilter  the event marker filter
     * @return                   true if the setup succeeded, false otherwise
     */
    public boolean setup(String serverName, int serverPort, String sinkName, String channelPath,
            File archiveDirectory, double startTime, double endTime, String eventMarkerFilter) {

        if (startTime >= endTime) {
            logger.debug("The start time must come before the end time.");
            return false;
        }

        setServerName(serverName);
        setServerPort(serverPort);

        this.sinkName = sinkName;
        this.channelPath = channelPath;
        this.archiveDirectory = archiveDirectory;
        this.startTime = startTime;
        this.endTime = endTime;
        this.eventMarkerFilter = eventMarkerFilter;

        return validateSetup();
    }

    /** 
     * Validates the setup.  This method prepares the RBNB connection, sets up the
     * time ranges for data archival, and then chacks the time ranges for validity.
     * 
     * @return  true if the setup is valid, false otherwise
     */
    private boolean validateSetup() {
        logger.debug("FileArchiverSink.validateSetup() called.");
        printSetup();

        if (!connect()) {
            return false;
        }

        if (!setupTimeRanges()) {
            return false;
        }

        if (!checkTimeRanges()) {
            return false;
        }

        return true;
    }

    /**
     * Prints the setup parameters.
     */
    private void printSetup() {
        logger.debug("FileArchiverSink.printSetup() called.");
        logger.debug("Starting FileArchiverSink on " + getServer() + " as " + sinkName);
        logger.debug("  Archiving channel " + channelPath);
        logger.debug("  to directory " + archiveDirectory);

        if (endTime != Double.MAX_VALUE) {
            logger.debug("  from " + RBNBUtilities.secondsToISO8601(startTime) + " to "
                    + RBNBUtilities.secondsToISO8601(endTime));
        } else if (startTime != 0) {
            logger.debug("  from " + RBNBUtilities.secondsToISO8601(startTime));
        }

        if (eventMarkerFilter != null) {
            logger.debug("  using event marker filter " + eventMarkerFilter);
        }
    }

    /**
     * Sets up the time ranges based on the start and stop times and the event
     * marker filter.
     * 
     * @return  true if the time ranges are setup
     */
    private boolean setupTimeRanges() {
        logger.debug("FileArchiverSink.setupTimeRanges() called.");
        if (eventMarkerFilter == null) {
            timeRanges = new ArrayList<TimeRange>();
            timeRanges.add(new TimeRange(startTime, endTime));
        } else {
            try {
                timeRanges = MarkerUtilities.getTimeRanges(sink, eventMarkerFilter, startTime, endTime);
            } catch (SAPIException e) {
                logger.debug("Error retreiving event markers from server.");
                return false;
            } catch (IllegalArgumentException e) {
                logger.debug("Error: The event marker filter format is invalid.");
                return false;
            }
        }

        return true;
    }

    /**
     * Checks the time ranges to see if they are valid and have data.
     * 
     * @return  true if the time ranges are valid
     */
    private boolean checkTimeRanges() {
        logger.debug("FileArchiverSink.checkTimeRanges() called.");
        Node channelMetadata;
        try {
            channelMetadata = RBNBUtilities.getMetadata(getServer(), channelPath);
        } catch (SAPIException e) {
            logger.debug("Error retreiving channel metadata from the server.");
            return false;
        }

        if (channelMetadata == null) {
            logger.debug("Error: Channel " + channelPath + " not found.");
            return false;
        }

        double currentTime = System.currentTimeMillis() / 1000d;

        double channelStartTime = channelMetadata.getStart();
        double channelEndTime = channelStartTime + channelMetadata.getDuration();
        TimeRange channelTimeRange = new TimeRange(channelStartTime, channelEndTime);

        for (int i = 0; i < timeRanges.size(); i++) {
            TimeRange timeRange = timeRanges.get(i);

            // allow end times in the future
            if (timeRange.getEndTime() > currentTime) {
                continue;
            }

            // skip time ranges in the past where there is no data
            if (!timeRange.intersects(channelTimeRange)) {
                logger.debug("Warning: Skipping the time range from "
                        + RBNBUtilities.secondsToISO8601(timeRange.getStartTime()) + " to "
                        + RBNBUtilities.secondsToISO8601(timeRange.getEndTime())
                        + " since there are no data for it.");
                timeRanges.remove(i--);
            }
        }

        if (timeRanges.size() == 0) {
            logger.debug("Error: There are no data for the specified time ranges.");
            logger.debug("There is data from " + RBNBUtilities.secondsToISO8601(channelStartTime) + " to "
                    + RBNBUtilities.secondsToISO8601(channelEndTime) + ".");

            return false;
        }

        // move up start time to first data point
        TimeRange firstTimeRange = timeRanges.get(0);
        if (firstTimeRange.getStartTime() < channelTimeRange.getStartTime()) {
            logger.debug("Warning: Setting start time to "
                    + RBNBUtilities.secondsToISO8601(channelTimeRange.getStartTime())
                    + " since there is no data before it.");
            firstTimeRange.setStartTime(channelTimeRange.getStartTime());
        }

        return true;
    }

    /**
     * Gets the start time.
     * 
     * @return  the start time
     */
    public double getStartTime() {
        return startTime;
    }

    /**
     * Gets the end time.
     * 
     * @return  the end time
     */
    public double getEndTime() {
        return endTime;
    }

    /**
     * Gets the event marker filter.
     * 
     * @return  the event marker filter
     */
    public String getEventMarkerFilter() {
        return eventMarkerFilter;
    }

    /**
     * A method that gets the archive interval 
     * 
     * @return the archive interval in seconds
     */
    public int getArchiveInterval() {
        return this.archiveInterval;
    }

    /**
     * A method that sets archive interval (in seconds) 
     *
     * @param interval  the archive interval (in seconds)
     */
    public void setArchiveInterval(int interval) {
        this.archiveInterval = interval;
    }

    /**
     * Export data to disk.
     */
    public boolean export() {
        logger.debug("FileArchiverSink.export() called.");
        doExport = true;

        if (!runWork()) {
            return false;
        }

        doExport = false;

        return true;
    }

    /**
     * Stop exporting data to disk. This will return immediately.
     */
    public void stopExport() {
        logger.debug("FileArchiverSink.stopExport() called.");
        doExport = false;
    }

    /**
     * Sees if data are being exported.
     * 
     * @return  true if exporting, false otherwise
     */
    public boolean isExporting() {
        return isConnected();
    }

    /**
     * Exports the data.
     */
    private boolean runWork() {
        logger.debug("FileArchiverSink.runWork() called.");
        int dataFramesExported = 0;

        try {
            FileArchiveUtility.confirmCreateDirPath(archiveDirectory);

            ChannelMap sMap = new ChannelMap();
            sMap.Add(channelPath);

            if (timeRanges.get(timeRanges.size() - 1).getEndTime() == Double.MAX_VALUE) {
                duration = Double.MAX_VALUE;
            } else {
                duration = 0;
                for (TimeRange timeRange : timeRanges) {
                    duration += timeRange.getEndTime() - timeRange.getStartTime();
                }
            }

            double elapsedTime = 0;
            for (TimeRange timeRange : timeRanges) {
                if (timeRange.getEndTime() != Double.MAX_VALUE) {
                    logger.debug("Exporting data from " + RBNBUtilities.secondsToISO8601(timeRange.getStartTime())
                            + " to " + RBNBUtilities.secondsToISO8601(timeRange.getEndTime()) + ".");
                } else {
                    logger.debug("Exporting data from " + RBNBUtilities.secondsToISO8601(timeRange.getStartTime())
                            + ".");
                }

                if (!connect()) {
                    return false;
                }

                dataFramesExported += exportData(sMap, timeRange.getStartTime(), timeRange.getEndTime(), duration,
                        elapsedTime);
                disconnect();

                elapsedTime += timeRange.getEndTime() - timeRange.getStartTime();
                fireProgressUpdate(elapsedTime);

                if (!doExport) {
                    break;
                }
            }
        } catch (SAPIException e) {
            logger.debug("Error getting data from server: " + e.getMessage() + ".");
            return false;
        } catch (IOException e) {
            logger.debug("Error writing data to file: " + e.getMessage() + ".");
            return false;
        } finally {
            disconnect();
        }

        if (doExport) {
            logger.debug("Export complete. Wrote " + dataFramesExported + " data frames.              ");
        } else {
            logger.debug("Export stopped. Wrote " + dataFramesExported + " data frames.               ");
        }

        return true;
    }

    /**
     * Exports data for a time range.
     * 
     * @param map             the channel map
     * @param startTime       the start time for the data
     * @param endTime         the end time for the data
     * @param baseTime        the base elasped time for the export
     * @return                the number of data frames written to disk
     * @throws SAPIException  if there is an error getting the data from the
     *                        server
     * @throws IOException    if there is an error writing the file
     */
    private int exportData(ChannelMap map, double startTime, double endTime, double duration, double baseTime)
            throws SAPIException, IOException {
        logger.debug("FileArchiverSink.exportData() called.");

        //sink.Subscribe(map, startTime, 0.0, "absolute");
        sink.Subscribe(map, startTime, duration, "absolute");

        int frameCount = 0;
        int fetchRetryCount = 0;

        while (doExport) {
            ChannelMap m = sink.Fetch(1800000); // fetch with 3 min sec timeout

            if (m.GetIfFetchTimedOut()) {
                if (++fetchRetryCount < 10) {
                    logger.debug("Warning: Request for data timed out, retrying.");
                    continue;
                } else {
                    logger.debug("Error: Unable to get data from server.");
                    break;
                }
            } else {
                fetchRetryCount = 0;
            }

            int index = m.GetIndex(channelPath);
            if (index < 0) {
                break;
            }

            // convert sec to millisec
            double timestamp = m.GetTimes(index)[0];
            long unixTime = (long) (timestamp * 1000.0);
            File output = FileArchiveUtility.makePathFromTime(archiveDirectory, unixTime, filePrefix, filePathDepth,
                    fileExtension);

            if (FileArchiveUtility.confirmCreateDirPath(output.getParentFile())) {

                byte[] data = m.GetData(index);
                int numberOfFrames = (m.GetTimes(index)).length;

                FileOutputStream out = new FileOutputStream(output);
                for (int i = 0; i < data.length; i++) {

                    out.write(data[i]);
                    if (i % data.length / numberOfFrames == 0) {
                        frameCount++;
                    }
                }

                out.close();
                doExport = false;

                // test the file write success
                String newFileName = output.getPath();
                File latestDataFile = new File(newFileName);

                if (latestDataFile.length() > 0L) {
                    logger.info("Successful export to " + latestDataFile.getPath());

                } else {
                    logger.info("Unsuccessful export. File " + latestDataFile.getPath() + " was "
                            + latestDataFile.length() + " bytes.");

                }

            }

        }

        return frameCount;
    }

    /**
     * Connect to the RBNB server.
     * 
     * @return  true if connected, false otherwise
     */
    private boolean connect() {
        logger.debug("FileArchiverSink.connect() called.");
        if (isConnected()) {
            return true;
        }

        try {
            sink.OpenRBNBConnection(getServer(), sinkName);
        } catch (SAPIException e) {
            logger.debug("Error: Unable to connect to server.");
            disconnect();
            return false;
        }

        connected = true;

        return true;
    }

    /**
     * Disconnects from the RBNB server.
     */
    private void disconnect() {
        logger.debug("FileArchiverSink.disconnect() called.");
        if (!isConnected()) {
            return;
        }

        sink.CloseRBNBConnection();

        connected = false;
    }

    /**
     * Sees if we are connected to the RBNB server.
     * 
     * @return  true if connected, false otherwise
     */
    public boolean isConnected() {
        return connected;
    }

    /**
     * Adds a listener for the export progress.
     * 
     * @param listener  the listener to add
     */
    public void addTimeProgressListener(TimeProgressListener listener) {
        listeners.add(listener);
    }

    /**
     * Removes a listener for the export progress.
     * 
     * @param listener  the listener to remove
     */
    public void removeTimeProgressListener(TimeProgressListener listener) {
        listeners.remove(listener);
    }

    /**
     * Notifies listener of progress.
     * 
     * @param time  the elapsed time to notify
     */
    private void fireProgressUpdate(double time) {
        for (TimeProgressListener listener : listeners) {
            listener.progressUpdate(duration, time);
        }
    }

    /** A method that returns the CVS version string */
    protected String getCVSVersionString() {
        return ("$LastChangedDate$\n" + "$LastChangedRevision$" + "$LastChangedBy$" + "$HeadURL$");
    }

}