/** * Copyright 2014 Comcast Cable Communications Management, LLC * * This file is part of CATS. * * CATS 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 3 of the License, or * (at your option) any later version. * * CATS 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 CATS. If not, see <http://www.gnu.org/licenses/>. */ package com.comcast.cats.service.power; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static com.comcast.cats.service.power.util.PowerConstants.NEWLINE; /** * This class is designed to provide remote connections to the * power devices using a simple socket. * */ public class PowerDeviceConnection { private String host; private InetAddress inetAddress; private int port; private Socket socket; protected BufferedReader inFromServer; protected OutputStream outToServer; private final Logger log = LoggerFactory.getLogger(PowerDeviceConnection.class); /** * Used for determining if to send an initial NEWLINE upon a successful socket connection to remote host */ private boolean initialCR = false; /** * The constructor to create the non-connected object, * based on the remote address and port. * * @param host - the InetAddress of the remote device * @param port - the port of the remote device */ public PowerDeviceConnection(InetAddress host, int port) { this(host.toString(), port); } /** * The constructor to create the non-connected object, * based on remote address and port. * * @param host - the string address representation of the remote device * @param port - the port of the remote device */ public PowerDeviceConnection(String host, int port) { this.port = port; this.host = host; this.socket = new Socket(); } /** * Attempts to create connection with remote devices. * If a ConnectException is thrown, this will try to * connect again up to three times. * @param timeout The timeout in milliseconds. * @param currTime Current number of seconds. * @return <b>true</b> on success. */ private boolean connectTry(final int timeout, int currTime) { long start = System.currentTimeMillis(); try { this.socket = new Socket(); inetAddress = InetAddress.getByName(host); this.socket.connect(new InetSocketAddress(inetAddress, port), timeout); try { outToServer = socket.getOutputStream(); inFromServer = new BufferedReader( new InputStreamReader(socket.getInputStream())); } catch (IOException ioe) { close(); } if (initialCR) { sendCmd(NEWLINE, true); } return isConnected(); } catch (java.net.SocketTimeoutException ste) { log.error("Connection timed out; waited " + timeout + " milliseconds."); } catch (java.net.UnknownHostException uhe) { log.error("UnknownHostException: " + uhe.getMessage()); } catch (java.net.ConnectException cone) { // This is probably because a client is already connected to the // power device and in middle of sending a command lets retry // until we hit the timeout. retrySleep(); log.error("ConnectException, about to retry: " + cone.getMessage()); currTime += System.currentTimeMillis() - start; if (currTime < timeout) { return connectTry(timeout, currTime); } } catch (Exception e) { log.error("Exception: " + e.getMessage()); } return false; } private void retrySleep() { try { Thread.sleep(300); }catch(Exception e) { log.error("Retry sleep"); } } /** * This method tries to establish a connection with the remote device * based on three criteria: successful creation of an InetAddress, the * success of the connection and the establishment of the input and output * data streams. If any of the above fail, the connection returns FALSE * * @param timeout - the amount of time in millisec. to wait for the connection. * @return boolean - true if all criteria was met and connection was made, false otherwise */ public boolean connect(final int timeout) { if (timeout <= 0) { throw new IllegalArgumentException("timeout must be > 0"); } return connectTry(timeout, 0); } /** * This method writes out a string as an array of bytes to the remote host. * Before it writes it verifies that the string terminates with NEWLINE * characters, if not, it appends them to the string before transmission. * These characters can be changed by modifying the NEWLINE field. * * @param cmd - the string to transmit to the remote host * @param echo - set true if you want the string to be echoed back to the console, or false for quiet mode * @return boolean - true if the string was successfully transmitted, false otherwise */ public boolean sendCmd(String cmd, final boolean echo) { if (!cmd.endsWith(NEWLINE)) { cmd += NEWLINE; } try { if (null != this.outToServer) { this.outToServer.write(cmd.getBytes()); if (echo) { log.info("Writing to " + host + ": [" + cmd.replaceAll("(\\r|\\n)", "") + "]"); } this.outToServer.flush(); return true; } } catch (IOException ioe) { log.error("IOException: " + ioe.getMessage()); } return false; } /** * This method sets the socket timeout for the amount of time to wait for blocking * methods, such as reading from the socket input stream * * @param timeout - amount of time to block in milliseconds * */ protected void setSoTimeout(final int timeout) { try { if (timeout == this.socket.getSoTimeout()) { return; } this.socket.setSoTimeout(timeout); } catch (java.net.SocketException se) { log.error("SocketException: " + se.getMessage()); } } /** * This method reads a character at a time from the socket input stream until EOF is reached, * or stream is closed. This is a blocking method until -1 is returned by inputStream, for more * information refer to the socket api * * @param timeout - amount of time to block in milliseconds before raising a java.net.SocketTimeoutException * @return String - the string that it reads from the socket input stream */ public String read(final int timeout) { StringBuilder buff = new StringBuilder(); setSoTimeout(timeout); try { int c = inFromServer.read(); while (-1 != c) { buff.append((char) c); c = inFromServer.read(); } } catch (java.net.SocketTimeoutException ste) { log.debug("SocketTimeoutException (likely expected): " + ste.getMessage()); } catch (IOException ioe) { log.error("IOException: " + ioe.getMessage()); } return buff.toString(); } /** * This method reads a character at a time from the socket input stream until EOF is reached, * or the substring of the search string is found. Once string is found the input stream is interrupted * Immediately and returns * * @param string - the specific string that the user is waiting on from the socket input stream * @param timeout - amount of time to block in milliseconds before raising a java.net.SocketTimeoutException * @return String - the string that it reads from the socket input stream up to the search string, * everything after is truncated */ public String waitForString(String string, final int timeout) { StringBuilder buff = new StringBuilder(); setSoTimeout(timeout); try { int c = inFromServer.read(); while (-1 != c) { buff.append((char) c); if (buff.indexOf(string) != -1) { break; } c = inFromServer.read(); } } catch (java.net.SocketTimeoutException ste) { log.error(ste.getClass().getName() + " caught: " + ste.getMessage()); } catch (IOException ioe) { log.error(ioe.getClass().getName() + " caught: " + ioe.getMessage()); } return buff.toString(); } /** * This method is used to request that an initial carriage return is sent right after a * connection to the remote host has been established. * * @param initialCR - true if you want the initial NEWLINE to be sent when a connection is made, * false otherwise */ public void setInitialCR(final boolean initialCR) { this.initialCR = initialCR; } /** * This method retrieve the initialCR value. * * @return boolean - true if initial carriage return at connection has been set, false otherwise */ public boolean getInitialCR() { return this.initialCR; } /** * This method verifies if the input stream was established. * * @return boolean - true if socket input stream has been established, false otherwise */ public boolean hasInputStream() { return (null != inFromServer); } /** * This method verifies that the connection has been established and is alive. * * @return boolean - true if socket is connected and not closed, false otherwise */ public boolean isConnected() { return (this.socket.isConnected() && (!this.socket.isClosed())); } /** * This method retrieves the remote host address as String. * * @return String - the host address that socket connection was requested to */ public String getHost() { return this.host; } /** * This method retrieves the remote host address as a InetAddress object. * * @return InetAddress - the host address that socket connection was requested to */ public InetAddress getInetAddress() { return this.inetAddress; } /** * This method retrieves the remote host port. * * @return int - the remote host port the connection was requested for */ public int getPort() { return this.port; } /** * This method cleans up by closing socket and any socket input/output * streams that have been opened. * */ public void close() { try { this.outToServer = null; this.inFromServer = null; if (null != socket) { this.socket.close(); if (!this.socket.isClosed()) { log.warn("Failed to close socket"); } } } catch (Exception e) { log.error(e.getClass().getName() + " caught: " + e.getMessage()); } } }