edu.hawaii.soest.kilonalu.adam.AdamDispatcher.java Source code

Java tutorial

Introduction

Here is the source code for edu.hawaii.soest.kilonalu.adam.AdamDispatcher.java

Source

/**
 *  Copyright: 2007 Regents of the University of Hawaii and the
 *             School of Ocean and Earth Science and Technology
 *    Purpose: To read Advantech ADAM 60XX binary engineering data over UDP
 *             communication and forward packet data to AdamSource drivers for
 *             conversion and upload to the RBNB DataTurbine.
 *    Authors: Christopher Jones
 *
 * $HeadURL$
 * $LastChangedDate$
 * $LastChangedBy$
 * $LastChangedRevision$
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package edu.hawaii.soest.kilonalu.adam;

import com.rbnb.sapi.Source;
import com.rbnb.sapi.SAPIException;

import java.lang.StringBuffer;
import java.lang.StringBuilder;
import java.lang.InterruptedException;

import java.io.File;
import java.io.PrintWriter;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.DataInputStream;
import java.io.IOException;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;

import java.nio.ByteBuffer;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;

import org.apache.commons.codec.binary.Hex;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.XMLConfiguration;

import org.apache.commons.lang.exception.NestableException;

import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;

/**
 * A simple class used to harvest a decimal ASCII data stream from an Advantech
 * ADAM 6XXX module over a UDP socket connection  from a 
 * serial2ip converter host. The data stream is then converted into RBNB frames 
 * and pushed into the RBNB DataTurbine real time server using multiple AdamSource
 * objects, depending on which address the datagram originates from. 
 */
public class AdamDispatcher {

    /**
     * The default log configuration file location
     */
    private final String DEFAULT_LOG_CONFIGURATION_FILE = "lib/log4j.properties";

    /**
     * The log configuration file location
     */
    private String logConfigurationFile = DEFAULT_LOG_CONFIGURATION_FILE;

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

    /**
     * The XML configuration file location for the list of sensor properties
     */
    private String xmlConfigurationFile = "lib/sensor.properties.xml";

    /**
     * The XML configuration object with the list of sensor properties
     */
    private XMLConfiguration xmlConfiguration;

    /*
     *  A default source IP address for the source sensor data
     */
    private final String DEFAULT_SOURCE_HOST_NAME = "localhost";

    /**
     * The domain name or IP address of the host machine that this Source 
     * represents and from which the data will stream. 
     */
    private String sourceHostName = DEFAULT_SOURCE_HOST_NAME;

    /*
     *  A default source UDP port for the source sensor data
     */
    private final int DEFAULT_SOURCE_HOST_PORT = 5168;

    /**
     * The UDP port to connect to on the Source host machine 
     */
    private int sourceHostPort = DEFAULT_SOURCE_HOST_PORT;

    /*
     * The default IP address or DNS name of the RBNB server
     */
    private static final String DEFAULT_SERVER_NAME = "localhost";

    /*
     * The default TCP port of the RBNB server
     */
    private static final int DEFAULT_SERVER_PORT = 3333;

    /*
     * The IP address or DNS name of the RBNB server
     */
    private String serverName = DEFAULT_SERVER_NAME;
    /*
     * The default TCP port of the RBNB server
     */
    private int serverPort = DEFAULT_SERVER_PORT;

    /*
     * The address and port string for the RBNB server
     */
    private String server = serverName + ":" + serverPort;

    /*
     * The default archive mode for RBNB Source clients
     */
    private static final String DEFAULT_ARCHIVE_MODE = "append";

    /*
     * The archive mode for RBNB Source clients
     */
    private String archiveMode = DEFAULT_ARCHIVE_MODE;

    /*
     * The default size of the ByteBuffer used to buffer the UDP stream from the
     * source instrument.
     */
    private int DEFAULT_BUFFER_SIZE = 256; // bytes

    /**
     * The size of the ByteBuffer used to buffer the UDP stream from the 
     * source instrument.
     */
    private int bufferSize = DEFAULT_BUFFER_SIZE;

    /*
     * The thread that is run for streaming data from the instrument
     */
    private Thread streamingThread;

    /*
     * The the connection status for the UDP port for the data streams
     */
    private boolean connected = false;

    /*
     * A boolean field indicating if the instrument connection is ready to stream
     */
    private boolean readyToStream = false;

    /*
     * The socket used to establish UDP communication with the instrument
     */
    private DatagramSocket datagramSocket;

    /*
     * The datagram object used to represent an individual incoming UDP packet
     */
    private DatagramPacket datagramPacket;

    /*
     * A hash map that contains IP address to RBNB Source mappings
     */
    private HashMap<String, AdamSource> sourceMap;

    /*
     * An internal Thread setting used to specify how long, in milliseconds, the
     * execution of the data streaming Thread should wait before re-executing
     * 
     * @see execute()
     */
    private final int RETRY_INTERVAL = 5000;

    /**
     * Constructor - create an empty instance of the AdamDispatcher object, using
     * default values for the RBNB server name and port, source instrument name
     * and port, archive mode, archive frame size, and cache frame size. 
     */
    public AdamDispatcher() {
    }

    /**
     * A method that executes the streaming of data from the source to the RBNB
     * server after all configuration of settings, connections to hosts, and
     * thread initiatizing occurs.  This method contains the detailed code for 
     * streaming the data and interpreting the stream.
     */
    protected boolean execute() {
        logger.info("AdamDispatcher.execute() called.");
        // do not execute the stream if there is no connection
        if (!isConnected())
            return false;

        boolean failed = false;

        // while data are being sent, read them into the buffer
        try {

            // Create a buffer that will store the sample bytes as they are read
            byte[] bufferArray = new byte[getBufferSize()];

            // and a ByteBuffer used to transfer the bytes to the parser
            ByteBuffer sampleBuffer = ByteBuffer.allocate(getBufferSize());

            this.datagramPacket = new DatagramPacket(bufferArray, bufferArray.length);

            // while there are bytes to read from the socket ...
            while (!failed) {

                // receive any incoming UDP packets and parse the data payload
                datagramSocket.receive(this.datagramPacket);

                logger.debug("Host: " + datagramPacket.getAddress() + " data: "
                        + new String(Hex.encodeHex(datagramPacket.getData())));

                // the address seems to be returned with a leading slash (/). Trim it.
                String datagramAddress = datagramPacket.getAddress().toString().replaceAll("/", "");

                sampleBuffer.put(datagramPacket.getData());

                // Given the IP address of the source UDP packet and the data ByteBuffer,
                // find the correct source in the sourceMap hash and process the data
                if (sourceMap.get(datagramAddress) != null) {

                    AdamSource source = sourceMap.get(datagramAddress);

                    // process the data using the AdamSource driver
                    source.process(datagramAddress, this.xmlConfiguration, sampleBuffer);

                } else {
                    logger.debug("There is no configuration information for " + "the ADAM module at "
                            + datagramAddress + ". Please add the configuration to the "
                            + "sensor.properties.xml configuration file.");
                }

                sampleBuffer.clear();

            } // end while (more socket bytes to read)

            disconnect();
            //      
        } catch (IOException e) {
            // handle exceptions
            // In the event of an i/o exception, log the exception, and allow execute()
            // to return false, which will prompt a retry.
            failed = true;
            e.printStackTrace();
            return !failed;

        }

        return !failed;
    }

    /**
     * A method used to connect to the UDP port of the host that will have the 
     * UDP stream of data packets, and that will also connect each of the AdamSource
     * drivers to the DataTurbine.
     */
    protected boolean connect() {
        if (isConnected()) {
            return true;
        }

        try {
            // bind to the UDP socket
            this.datagramSocket = new DatagramSocket(getHostPort());

            // Create a list of sensors from the properties file, and iterate through
            // the list, creating an RBNB Source object for each sensor listed. Store
            // these objects in a HashMap for later referral.

            this.sourceMap = new HashMap<String, AdamSource>();

            List sensorList = this.xmlConfiguration.getList("sensor.address");

            // declare the properties that will be pulled from the 
            // sensor.properties.xml file
            String address = "";
            String sourceName = "";
            String cacheSize = "";
            String archiveSize = "";
            String archiveChannel = "";

            // evaluate each sensor listed in the sensor.properties.xml file
            for (Iterator sIterator = sensorList.iterator(); sIterator.hasNext();) {

                // get each property value of the sensor
                int index = sensorList.indexOf(sIterator.next());
                address = (String) this.xmlConfiguration.getProperty("sensor(" + index + ").address");
                sourceName = (String) this.xmlConfiguration.getProperty("sensor(" + index + ").name");
                cacheSize = (String) this.xmlConfiguration.getProperty("sensor(" + index + ").cacheSize");
                archiveSize = (String) this.xmlConfiguration.getProperty("sensor(" + index + ").archiveSize");

                // given the properties, create an RBNB Source object
                AdamSource adamSource = new AdamSource(this.serverName, (new Integer(this.serverPort)).toString(),
                        this.archiveMode, (new Integer(archiveSize).intValue()),
                        (new Integer(cacheSize).intValue()), sourceName);
                adamSource.startConnection();
                sourceMap.put(address, adamSource);

            }
            connected = true;

        } catch (SocketException se) {
            System.err.println(
                    "Failed to connect to the UDP data stream. " + "The error message was: " + se.getMessage());

        }

        return connected;
    }

    /*
     * A method that returns the connection status
     */
    public boolean isConnected() {
        return connected;

    }

    /**
     * A method used to disconnect from the UDP port of the host that has the 
     * UDP stream of data packets, and that will also disconnect each of the 
     * AdamSource drivers from the DataTurbine.
     */
    protected void disconnect() {

        // disconnect from the UDP socket
        if (datagramSocket != null) {
            this.datagramSocket.close();

        }

        // get each source and disconnect from the DataTurbine
        for (Iterator sIterator = (sourceMap.keySet()).iterator(); sIterator.hasNext();) {

            AdamSource adamSource = sourceMap.get(sIterator.next());
            adamSource.stopConnection();
            logger.info("Disconnected from source: " + adamSource.getRBNBClientName());
        }
        connected = false;

    }

    /**
     * A method that sets the size, in bytes, of the ByteBuffer used in streaming 
     * data from a source instrument via a TCP connection
     */
    public int getBufferSize() {
        return this.bufferSize;
    }

    /**
     * A method that returns the domain name or IP address of the source 
     * instrument (i.e. the serial-to-IP converter to which it is attached)
     */
    public String getHostName() {
        return this.sourceHostName;
    }

    /**
     * A method that returns the TCP port of the source 
     * instrument (i.e. the serial-to-IP converter to which it is attached)
     */
    public int getHostPort() {
        return this.sourceHostPort;
    }

    /**
     * A method that returns true if the UDP DatagramSocket connection is 
     * established and if the data streaming Thread has been started
     */
    public boolean isRunning() {
        // return the connection status and the thread status
        return (isConnected() && readyToStream);
    }

    /**
     * The main method for running the code
     * @ param args[] the command line list of string arguments
     */
    public static void main(String args[]) {

        try {
            // create a new instance of the AdamDispatcher object, and parse the command 
            // line arguments and xml configuration as settings for this instance
            final AdamDispatcher adamDispatcher = new AdamDispatcher();

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

            logger.info("AdamDispatcher.main() called.");

            // parse the commandline arguments and the sensor configuration file
            // to configure the UDP source and RBNB serverconnections, then 
            // start the streaming connection between the source and the RBNB server.
            if (adamDispatcher.parseArgs(args) && adamDispatcher.parseConfiguration()) {
                adamDispatcher.start();
            }

            // Handle ctrl-c's and other abrupt death signals to the process
            Runtime.getRuntime().addShutdownHook(new Thread() {
                // stop the streaming process
                public void run() {
                    adamDispatcher.stop();
                }
            });

        } catch (Exception e) {
            logger.info("Error in main(): " + e.getMessage());
            e.printStackTrace();
        }
    }

    /*
     * A method that runs the data streaming work performed by the execute()
     * by handling execution problems and continuously trying to re-execute after 
     * a specified retry interval for the thread.
     */
    private void runWork() {

        // handle execution problems by retrying if execute() fails
        boolean retry = true;

        while (retry) {

            // connect to the UDP socket, and establish RBNB source connections for
            // each of the sensors
            if (connect()) {

                // run the data streaming code
                retry = !execute();
            }

            disconnect();

            if (retry) {
                try {
                    Thread.sleep(RETRY_INTERVAL);
                } catch (Exception e) {
                    logger.info("There was an execution problem. Retrying. Message is: " + e.getMessage());
                }
            }
        }
        // stop the streaming when we are done
        stop();
    }

    /*
     * A method used to parse the command line arguments and configure the UDP
     * and RBNB connections
     */
    private boolean parseArgs(String[] args) {

        CommandLine command;
        try {
            command = (new PosixParser()).parse(setOptions(), args);

        } catch (ParseException pe) {
            logger.info("There was an error parsing the command line options. "
                    + "Please be sure to use the correct options. The error message was: " + pe.getMessage());
            return false;
        }
        return setArgs(command);

    }

    /*
     * A method used to get the sensor configuration properties for each of
     * the listed ADAM sensors
     *
     * @param configurationFile - the name of the XML configuration file
     */
    private boolean parseConfiguration() {

        boolean failed = true;

        try {
            // create an XML Configuration object from the sensor XML file
            File xmlConfigFile = new File(this.xmlConfigurationFile);
            this.xmlConfiguration = new XMLConfiguration(xmlConfigFile);
            failed = false;

        } catch (NullPointerException npe) {
            logger.info("There was an error reading the XML configuration file. " + "The error message was: "
                    + npe.getMessage());

        } catch (ConfigurationException ce) {
            logger.info("There was an error creating the XML configuration. " + "The error message was: "
                    + ce.getMessage());

        }
        return !failed;

    }

    /**
     * A method that sets the command line arguments for this class.
     * 
     * @param command  The CommandLine object being passed in from the command
     */
    protected boolean setArgs(CommandLine command) {

        // handle the -h option
        if (command.hasOption('h')) {
            printUsage();
            return false;
        }

        // handle the -s option
        if (command.hasOption('s')) {
            String serverName = command.getOptionValue('s');
            if (serverName != null)
                setServerName(serverName);
        }

        // handle the -p option
        if (command.hasOption('p')) {
            String serverPort = command.getOptionValue('p');
            if (serverPort != null) {
                try {
                    setServerPort(Integer.parseInt(serverPort));

                } catch (NumberFormatException nf) {
                    System.out.println(
                            "Please enter a numeric value for -p (server port). " + serverPort + " is not valid.");
                    return false;
                }
            }
        }

        // handle the -H option
        if (command.hasOption("H")) {
            String hostName = command.getOptionValue("H");
            if (hostName != null) {
                setHostName(hostName);
            }
        }

        // handle the -P option, test if it's an integer
        if (command.hasOption("P")) {
            String hostPort = command.getOptionValue("P");
            if (hostPort != null) {
                try {
                    setHostPort(Integer.parseInt(hostPort));

                } catch (NumberFormatException nfe) {
                    logger.info("Please enter a numeric value for the host port. " + hostPort
                            + " is not a valid number.");
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * A method that sets the domain name or IP address of the source 
     * instrument (i.e. the serial-to-IP converter to which it is attached)
     *
     * @param hostName  the domain name or IP address of the source instrument
     */
    public void setHostName(String hostName) {
        this.sourceHostName = hostName;
    }

    /**
     * A method that sets the TCP port of the source 
     * instrument (i.e. the serial-to-IP converter to which it is attached)
     *
     * @param hostPort  the TCP port of the source instrument
     */
    public void setHostPort(int hostPort) {
        this.sourceHostPort = hostPort;
    }

    /**
     * A method that sets the domain name or IP address of the RBNB server
     *
     * @param serverName  the domain name or IP address of the RBNB server
     */
    public void setServerName(String serverName) {
        this.serverName = serverName;
    }

    /**
     * A method that sets the TCP port of the RBNB server 
     *
     * @param serverPort  the TCP port of the RBNB server
     */
    public void setServerPort(int serverPort) {
        this.serverPort = serverPort;
    }

    /**
     * A method that sets the command line options for this class.
     */
    protected Options setOptions() {
        Options options = new Options();

        options.addOption("h", false, "Print help");
        options.addOption("s", true, "RBNB Server Hostname");
        options.addOption("p", true, "RBNB Server Port Number");
        options.addOption("H", true, "Source host name or IP");
        options.addOption("P", true, "Source host port number");

        return options;
    }

    /**
     * A method that starts the streaming of data from the source instruments to
     * the RBNB server via an established TCP connection.  
     */
    public boolean start() {

        // return false if the streaming is running
        if (isRunning()) {
            return false;
        }

        // reset the connection to the RBNB server
        if (isConnected()) {
            disconnect();
        }
        connect();

        // return false if the connection fails
        if (!isConnected()) {
            return false;
        }

        // begin the streaming thread to the source
        startThread();

        return true;
    }

    /*
     * A method that creates and starts a new Thread with a run() method that 
     * begins processing the data streaming from the source instrument.
     */
    private void startThread() {

        // build the runnable class and implement the run() method
        Runnable runner = new Runnable() {
            public void run() {
                runWork();
            }
        };

        // build the Thread and start it, indicating that it has been started
        readyToStream = true;
        streamingThread = new Thread(runner, "StreamingThread");
        streamingThread.start();
    }

    /**
     * A method that stops the streaming of data between the source instruments and
     * the RBNB server.  
     */
    public boolean stop() {

        // return false if the thread is not running
        if (!isRunning()) {
            return false;
        }

        // stop the thread and disconnect from the RBNB server
        stopThread();
        disconnect();
        return true;
    }

    /*
     * A method that interrupts the thread created in startThread()
     */
    private void stopThread() {
        // set the streaming status to false and stop the Thread
        readyToStream = false;
        streamingThread.interrupt();
    }

    /**
     * Print out the usage of this application to standard output.
     */
    protected void printUsage() {
        HelpFormatter helpFormatter = new HelpFormatter();
        helpFormatter.printHelp(this.getClass().getName(), setOptions());

    }

    /**
     * A method that gets the log configuration file location
     *
     * @return logConfigurationFile  the log configuration file location
     */
    public String getLogConfigurationFile() {
        return this.logConfigurationFile;
    }

    /**
     * A method that sets the log configuration file name
     *
     * @param logConfigurationFile  the log configuration file name
     */
    public void setLogConfigurationFile(String logConfigurationFile) {
        this.logConfigurationFile = logConfigurationFile;
    }

}