io.cloudracer.mocktcpserver.tcpclient.TCPClient Maven / Gradle / Ivy
package io.cloudracer.mocktcpserver.tcpclient;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import io.cloudracer.mocktcpserver.datastream.DataStream;
import io.cloudracer.properties.ConfigurationSettings;
/**
* A TCP Client provided primarily for demonstration purposes, and for use in test suites.
*
* Send messages to a specified {@link TCPClient#TCPClient(String, int) host} (or localhost, if unspecified) on a specified {@link TCPClient#getPort() port}. By default the client will wait for a synchronous response from the {@link TCPClient#getHostName() server} but the response can be ignored (i.e. not waited for) for particular {@link TCPClient#send(String, boolean) send} instructions.
*
* If a {@link TCPClient#setResponseTerminator(byte[]) response terminator} is specified, the Client will wait for a synchronous response with that terminator, unless the {@link TCPClient#setACK(byte[]) ACK} or {@link TCPClient#setNAK(byte[]) NAK} response is received first. A custom ACK or NAK can be specified.
*
* @author John McDonnell
*/
public class TCPClient implements Closeable {
private final Logger logger = LogManager.getLogger(this.getClass().getSimpleName());
private final ConfigurationSettings configurationSettings = new ConfigurationSettings();
private static final byte[] DEFAULT_ACK = { 65 };
private byte[] ack;
private static final byte[] DEFAULT_NAK = { 78 };
private byte[] nak;
private static final byte[] DEFAULT_RESPONSE_TERMINATOR = { 13, 10 };
private byte[] responseTerminator = TCPClient.DEFAULT_RESPONSE_TERMINATOR;
private String hostName = null;
private Integer port = null;
private Socket socket;
private DataOutputStream dataOutputStream;
private DataInputStream dataInputStream;
/**
* Use the default port. Specify the {@link TCPClient#getPort() port} that the TCP {@link TCPClient#getHostName() server} is listening on.
*/
public TCPClient() {
this(null);
}
/**
* Use the specified port. Specify the {@link TCPClient#getPort() port} that the TCP {@link TCPClient#getHostName() server} is listening on.
*
* @param port the port that the TCP {@link TCPClient#getHostName() server} is listening on. If null, the default port will be used.
*/
public TCPClient(final Integer port) {
if (port == null) {
this.getPort();
} else {
this.setPort(port);
}
}
/**
* Specify the {@link TCPClient#getHostName() machine} to communication with and the {@link TCPClient#getPort() port} that the machine is listening on.
*
* @param hostName the machine name to communicate with.
* @param port the port number that the machine (specified by hostName) is listening on.
* @throws IOException see source documentation.
*/
public TCPClient(final String hostName, final int port) throws IOException {
this(port);
this.setHostName(hostName);
}
/**
* Close the socket (if it is open) and any open data streams.
*
* @throws IOException see source documentation.
*/
@Override
public void close() throws IOException {
this.setSocket(null);
}
/**
* Connect to the {@link TCPClient#getHostName() Server}.
*
* @throws IOException see source documentation.
*/
public void connect() throws IOException {
// Connect to the Server.
this.getSocket();
}
/**
* Send a message to the {@link TCPClient#getHostName() server} and wait for a response.
*
* @param message the message to send.
* @return the response from the {@link TCPClient#getHostName() server}.
* @throws IOException see source documentation.
*/
public DataStream send(final String message) throws IOException {
return this.send(message, true);
}
/**
* Send a message to the {@link TCPClient#getHostName() server} and, optionally, wait for a response.
*
* @param message the message to send.
* @param waitForResponse if true, wait for a response from the {@link TCPClient#getHostName() server}, otherwise null is returned.
* @return the response from the {@link TCPClient#getHostName() server} or null if waitForResponse is false.
* @throws IOException see source documentation.
*/
public DataStream send(final String message, final boolean waitForResponse) throws IOException {
return this.send(message, waitForResponse, this.getResponseTerminator());
}
/**
* Send a message down the socket.
*
* @param message the message to send.
* @param waitForResponse if true, wait for a response from the {@link TCPClient#getHostName() server}, otherwise return null.
* @param responseTerminator the terminator to wait for on the response. Ignored if null.
* @return the response from {@link TCPClient#getHostName() server} or null if waitForResponse is false.
* @throws IOException
*/
private DataStream send(final String message, final boolean waitForResponse, final byte[] responseTerminator) throws IOException {
this.logger.info(String.format("Sending the message %s.", message));
this.getDataOutputStream().write(message.getBytes(), 0, message.getBytes().length);
if (waitForResponse) {
try {
if (responseTerminator == null) {
return this.getResponse();
} else {
return this.getResponse(responseTerminator);
}
} catch (final TCPClientUnexpectedResponseException e) {
this.logger.error(e.getMessage(), e);
this.close();
return null;
}
} else {
return null;
}
}
/**
* Read and return the response message sent by {@link TCPClient#getHostName() server}.
*
* @return the response from the {@link TCPClient#getHostName() server}.
* @throws IOException see source documentation.
* @throws TCPClientUnexpectedResponseException see source documentation.
*/
public DataStream getResponse() throws IOException, TCPClientUnexpectedResponseException {
return this.getResponse(this.getResponseTerminator());
}
/**
* Read and return the response message sent by {@link TCPClient#getHostName() server}.
*
* @param terminator the response terminator. If null, only the {@link TCPClient#getACK() ACK} or {@link TCPClient#getNAK() NAK} will be expected and an exception will be throws if neither are received..
* @return the response from the {@link TCPClient#getHostName() server}.
* @throws UnsupportedEncodingException
* @throws IOException
* @throws TCPClientUnexpectedResponseException
*/
private DataStream getResponse(final byte[] terminator) throws IOException, TCPClientUnexpectedResponseException {
this.setDataInputStream(new DataInputStream(this.getSocket().getInputStream()));
final DataStream dataStream;
if (terminator == null) {
dataStream = new DataStream(this.getClass().getSimpleName());
} else {
dataStream = new DataStream(terminator.length, this.getClass().getSimpleName());
}
while (dataStream.write(this.getDataInputStream().read()) != -1) {
if (this.isTerminated(dataStream, terminator)) {
break;
}
}
return dataStream;
}
private boolean isTerminated(final DataStream dataStream, final byte[] terminator) throws TCPClientUnexpectedResponseException {
final boolean terminated = Arrays.equals(dataStream.getTail(), terminator)
|| Arrays.equals(dataStream.toByteArray(), this.getACK())
|| Arrays.equals(dataStream.toByteArray(), this.getNAK());
if (terminator == null && !terminated && (dataStream.size() == this.getACK().length || dataStream.size() == this.getNAK().length)) {
throw new TCPClientUnexpectedResponseException(dataStream);
}
return terminated;
}
/**
* The port that the {@link TCPClient#getHostName() Server} is listening on.
*
* @return the port number.
*/
public int getPort() {
if (this.port == null) {
try {
this.port = this.configurationSettings.getPort();
} catch (final ConfigurationException e) {
this.logger.error(e.getMessage(), e);
}
}
return this.port;
}
/**
* Set the port that the {@link TCPClient#getHostName() Server} is listening on.
*
* @param port the port number. If null, the default port will be used.
*/
private void setPort(final int port) {
this.port = port;
}
/**
* The Machine Name to send messages too.
*
* @return the Machine Name of the server to communicate with.
* @throws UnknownHostException see source documentation.
*/
public String getHostName() throws UnknownHostException {
if (this.hostName == null) {
final InetAddress host = InetAddress.getLocalHost();
this.hostName = host.getHostName();
}
return this.hostName;
}
/**
* Set the Machine Name to send messages too.
*
* @param hostName the Machine Name to send messages too.
*/
private void setHostName(final String hostName) {
this.hostName = hostName;
}
/**
* The NAK (i.e. Not Acknowledged) response to expect from the {@link TCPClient#getHostName() Server}.
*
* @return the NAK response to expect.
*/
public byte[] getNAK() {
if (this.nak == null) {
this.nak = TCPClient.DEFAULT_NAK;
}
return this.nak;
}
/**
* The NAK (i.e. Not Acknowledged) response to expect from the {@link TCPClient#getHostName() Server}.
*
* @param nak the NAK response to expect.
*/
public void setNAK(final byte[] nak) {
this.nak = nak;
}
/**
* The ACK (i.e. Acknowledged) response to expect from the {@link TCPClient#getHostName() Server}.
*
* @return the ACK response to expect.
*/
public byte[] getACK() {
if (this.ack == null) {
this.ack = TCPClient.DEFAULT_ACK;
}
return this.ack;
}
/**
* The ACK (i.e. Acknowledged) response to expect from the {@link TCPClient#getHostName() Server}.
*
* @param ack the ACK response to expect.
*/
public void setACK(final byte[] ack) {
this.ack = ack;
}
/**
* The response terminator to expect from the {@link TCPClient#getHostName() Server}.
*
* If null, all responses other than {@link TCPClient#getACK() ACK} or {@link TCPClient#getNAK() NAK} will result in an {@link TCPClientUnexpectedResponseException exception} (assuming responses are being waited for).
*
* @return the response terminator.
*/
public byte[] getResponseTerminator() {
return this.responseTerminator;
}
/**
* The response terminator to expect from the {@link TCPClient#getHostName() Server}.
*
* If null, all responses other than {@link TCPClient#getACK() ACK} or {@link TCPClient#getNAK() NAK} will result in an {@link TCPClientUnexpectedResponseException exception} (assuming responses are being waited for).
*
* @param responseTerminator the response terminator.
*/
public void setResponseTerminator(final byte[] responseTerminator) {
this.responseTerminator = responseTerminator;
}
/**
* Open a Socket, if not already open.
*
* @return an open {@link Socket} to the local machine, on the specified port ({@link TCPClient#getPort()}).
* @throws IOException
*/
private Socket getSocket() throws IOException {
if (this.socket == null) {
try {
this.socket = new Socket(this.getHostName(), this.getPort());
} catch (final IOException e) {
throw new IOException(String.format("Unable to connect to the Server \"%s\" on the port %d.", this.getHostName(), this.getPort()), e);
}
}
return this.socket;
}
private void setSocket(final Socket socket) throws IOException {
if (socket == null && this.socket != null) {
this.setDataInputStream(null);
this.setDataOutputStream(null);
IOUtils.closeQuietly(this.socket);
/*
* If this pause is not done here, a test that *immediately* tries to connect, may get a "connection refused" error.
*/
try {
final long sleepDuration = 20;
TimeUnit.MILLISECONDS.sleep(sleepDuration);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
}
this.socket = socket;
}
private DataOutputStream getDataOutputStream() throws IOException {
if (this.dataOutputStream == null) {
this.setDataOutputStream(new DataOutputStream(this.getSocket().getOutputStream()));
}
return this.dataOutputStream;
}
private void setDataOutputStream(final DataOutputStream dataOutputStream) throws IOException {
if (dataOutputStream == null && this.dataOutputStream != null) {
IOUtils.closeQuietly(this.getDataOutputStream());
}
this.dataOutputStream = dataOutputStream;
}
private DataInputStream getDataInputStream() throws IOException {
if (this.dataInputStream == null) {
this.setDataInputStream(new DataInputStream(this.getSocket().getInputStream()));
}
return this.dataInputStream;
}
private void setDataInputStream(final DataInputStream dataInputStream) throws IOException {
if (dataInputStream == null && this.dataInputStream != null) {
IOUtils.closeQuietly(this.getDataInputStream());
}
this.dataInputStream = dataInputStream;
}
}