All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.cloudracer.mocktcpserver.MockTCPServer Maven / Gradle / Ivy

There is a newer version: 1.7.0
Show newest version
package io.cloudracer.mocktcpserver;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.Assert;

import io.cloudracer.mocktcpserver.datastream.DataStream;
import io.cloudracer.mocktcpserver.datastream.DataStreamRegexMatcher;
import io.cloudracer.properties.ConfigurationSettings;

/**
 * A TCP Server that is designed to simulate success and failure conditions in System/Integration test environments.
 *
 * @author John McDonnell
 */
public class MockTCPServer extends Thread implements Closeable {

    private final Logger logger = LogManager.getLogger(this.getRootLoggerName());

    private enum Status {
        OPEN, CLOSING, CLOSED
    }

    private static final byte[] DEFAULT_TERMINATOR = { 13, 10, 10 };
    private static final byte[] DEFAULT_ACK = { 65 };
    private static final byte[] DEFAULT_NAK = { 78 };

    private byte[] terminator = null;
    private byte[] ack = null;
    private byte[] nak = null;

    private AssertionError assertionError;

    private ServerSocket socket;
    private BufferedReader inputStream;
    private DataOutputStream outputStream;
    private DataStreamRegexMatcher expectedMessage;

    private DataStream dataStream;
    private Socket connectionSocket;

    private Integer port;
    private boolean setIsAlwaysNAKResponse = false;
    private boolean setIsAlwaysNoResponse = false;
    private boolean isCloseAfterNextResponse = false;
    private int messagesReceivedCount = 0;

    private Status status = Status.OPEN;
    private final ConfigurationSettings configurationSettings = new ConfigurationSettings();

    private abstract static class Print {

        private static final Logger logger = LogManager.getLogger(Print.class.getName());

        private Print() {
            // Do nothing. This class cannot be constructed.
        }

        private static void printVersion() {
            final String ideWorkingFolder = "classes";

            final File executableLocation = new File(MockTCPServer.class.getProtectionDomain()
                    .getCodeSource()
                    .getLocation()
                    .getPath());
            String version = executableLocation.getName();
            // If this is the IDE, use the full path.
            if (version.equals(ideWorkingFolder)) {
                version = executableLocation.getAbsoluteFile().toString();
            }

            final Pattern pattern = Pattern.compile("\\d+\\.\\d+\\.\\d+");
            final Matcher matcher = pattern.matcher(version);
            if (matcher.find()) {
                logger.info(matcher.group(0));
            } else {
                logger.info("Version number cannot be identified.");
            }
        }

        private static void printHelp() {
            printHelp(null);
        }

        private static void printHelp(final ParseException e1) {
            if (e1 != null) {
                logger.info(String.format("Invalid command line: %s", e1.getMessage()));
            }

            // automatically generate the help statement
            final HelpFormatter formatter = new HelpFormatter();
            formatter.printHelp("MockTCPServer", getCommandLineOptions());
        }
    }

    /**
     * Start the server on the default port. This constructor is the equivalent of passing null to the constructor {@link MockTCPServer#MockTCPServer(Integer)}.
     */
    public MockTCPServer() {
        this(null);
    }

    /**
     * Start the server on the specified port.
     *
     * @param port the TCP Server will listen on this port. If null, the default port will be used.
     */
    public MockTCPServer(final Integer port) {
        this.logger.info("Starting...");

        if (port != null) {
            this.setPort(port);
        }

        super.setName(String.format("%s-%d", this.getThreadName(), this.getPort()));

        this.start();
        /*
         * 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 allows an operator to start a stand-alone MockTCPServer. The startup.sh(cmd) file can be used to start the server on a command-line.
     * 

* Note: currently, there is no packaged bundle to include all the dependencies and scripts. * * @param args an alternative port number can be passed as the first (and only) parameter. */ public static void main(String[] args) { final Logger logger = LogManager.getLogger(); try { final CommandLine commandLine = new DefaultParser().parse(getCommandLineOptions(), args); // Version information only. if (commandLine.hasOption("version")) { Print.printVersion(); } else if (commandLine.hasOption("help")) { Print.printHelp(); } else { final MockTCPServer mockTCPServer; if (commandLine.hasOption("port")) { final int port = Integer.parseInt(commandLine.getOptionValue("port")); mockTCPServer = new MockTCPServer(port); } else { mockTCPServer = new MockTCPServer(); } // When the Operating System interrupts the thread (kill or CTRL-C), stop the server. Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { logger.info("Operating System interrupt detected."); mockTCPServer.close(); } }); try { waitForThread(logger, mockTCPServer, 0); } finally { IOUtils.closeQuietly(mockTCPServer); } } } catch (final ParseException e1) { Print.printHelp(e1); } } private static void waitForThread(final Logger logger, final Thread thread, final long maximumDurationToWait) { try { thread.join(maximumDurationToWait); } catch (final InterruptedException e) { logger.warn(e); Thread.currentThread().interrupt(); } } @Override public void run() { try { while (this.getStatus() == Status.OPEN && this.getSocket() != null) { this.readIncomingStream(); } } catch (final SocketException e) { this.logger.warn(e); } catch (final Exception e) { this.logger.error(e.getMessage(), e); } finally { this.setStatus(Status.CLOSING); this.close(); } } private void readIncomingStream() throws IOException { this.setDataStream(null); while (this.getDataStream().write(this.getInputStream().read()) != -1) { if (Arrays.equals(this.getDataStream().getTail(), this.getTerminator())) { this.incrementMessagesReceivedCount(); break; } } if (this.getDataStream().getLastByte() == -1) { // The stream has ended so close all streams so that a new ServerSocket is opened and a new connection can be accepted. this.closeStreams(); } else if (this.getDataStream().size() > 0) { // Ignore null (i.e. zero length) in order allow a probing ping e.g. paping.exe this.processIncomingMessage(); } } private void processIncomingMessage() throws IOException { this.setAssertionError(null); try { if (this.getExpectedMessage() != null) { Assert.assertThat("Unexpected message from the AM Host Client.", this.getDataStream(), this.getExpectedMessage()); } } catch (final AssertionError e) { this.setAssertionError(e); } this.onMessage(this.getDataStream()); // If the stream has not ended and a response is required, send one. if (this.getDataStream().getLastByte() != -1 && !this.getIsAlwaysNoResponse()) { byte[] response; if (this.getAssertionError() == null && !this.getIsAlwaysNAKResponse()) { response = this.getACK(); } else { response = this.getNAK(); } this.getOutputStream().write(response); this.afterResponse(response); } } /** * The server will read the stream until these characters are encountered. * * @return the terminator. */ public byte[] getTerminator() { if (this.terminator == null) { this.terminator = MockTCPServer.DEFAULT_TERMINATOR; } return this.terminator; } /** * The server will read the stream until these characters are encountered. * * @param terminator the terminator. */ public synchronized void setTerminator(final byte[] terminator) { this.terminator = terminator; } /** * The positive acknowledgement response. * * @return positive acknowledgement */ public byte[] getACK() { if (this.ack == null) { this.ack = MockTCPServer.DEFAULT_ACK; } return this.ack; } /** * The positive acknowledgement response. * * @param ack positive acknowledgement */ public synchronized void setACK(final byte[] ack) { this.ack = ack; } /** * The negative acknowledgement response. * * @return negative acknowledgement */ public byte[] getNAK() { if (this.nak == null) { this.nak = MockTCPServer.DEFAULT_NAK; } return this.nak; } /** * The negative acknowledgement response. * * @param nak negative acknowledgement */ public synchronized void setNAK(final byte[] nak) { this.nak = nak; } /** * A server callback when a message has been processed, and a response has been sent to the client. * * @param response the response that has been sent. */ public synchronized void afterResponse(final byte[] response) { this.logger.debug(String.format("Sent the response: %s.", new String(response))); if (this.getIsCloseAfterNextResponse()) { this.setStatus(Status.CLOSED); } } /** * A server callback when a message is received. * * @param message the message received. */ public void onMessage(final DataStream message) { this.logger.info(String.format("Received: %s.", message.toString())); } /** * An error is recorded if a message other than that which is expected is received. * * @return a recorded error. */ public AssertionError getAssertionError() { return this.assertionError; } /** * An error will be recorded if a message other than that which is {@link MockTCPServer#getAssertionError() expected} is received. * * @param assertionError a recorded error. */ private void setAssertionError(final AssertionError assertionError) { this.assertionError = assertionError; } /** * Forces the Server to return a NAK in response to the next message received (regardless of any other conditions). The next message will first be processed as normal; irrespective of this property. *

* This is intended to be used to test a clients response to receiving a NAK. *

* Default is false. * * @return If true, the Servers next response will always be a NAK. */ public boolean getIsAlwaysNAKResponse() { return this.setIsAlwaysNAKResponse; } /** * Forces the Server to return a NAK in response to the next message received (regardless of any other conditions). The next message will first be processed as normal; irrespective of this property. *

* This is intended to be used to test a clients response to receiving a NAK. *

* Default is false. * * @param isAlwaysNAKResponse if true, the Servers next response will always be a NAK. */ public synchronized void setIsAlwaysNAKResponse(final boolean isAlwaysNAKResponse) { this.setIsAlwaysNAKResponse = isAlwaysNAKResponse; } /** * The server never return a response, when true. * * @return true when the server will never return a response. Default is false. */ public boolean getIsAlwaysNoResponse() { return this.setIsAlwaysNoResponse; } /** * The server never return a response, when true. * * @param isAlwaysNoResponse true when the server will never return a response. Default is false. */ public synchronized void setIsAlwaysNoResponse(final boolean isAlwaysNoResponse) { this.setIsAlwaysNoResponse = isAlwaysNoResponse; } /** * Forces the Server to close down after processing the next message received (regardless of any other conditions). The next message will first be processed as normal; irrespective of this property. *

* This is intended to be used so that test clients can wait on the server Thread to end. *

* Default is false. * * @return if true, the Server will close after the message processing is complete. Default is false. */ public boolean getIsCloseAfterNextResponse() { return this.isCloseAfterNextResponse; } /** * Forces the Server to close down after processing the next message received (regardless of any other conditions). The next message will first be processed as normal; irrespective of this property. *

* This is intended to be used so that test clients can wait on the server Thread to end. *

* Default is false. * * @param isCloseAfterNextResponse if true, the Server will close after the message processing is complete. Default is false. */ public synchronized void setIsCloseAfterNextResponse(final boolean isCloseAfterNextResponse) { this.isCloseAfterNextResponse = isCloseAfterNextResponse; } /** * If any message, other that this one, is the next message to be received, record it as an {@link MockTCPServer#setAssertionError(AssertionError) assertion error}. * * @return ignore if null. */ public DataStreamRegexMatcher getExpectedMessage() { return this.expectedMessage; } /** * If any message, other that this one, is the next message to be received, record it as an {@link MockTCPServer#setAssertionError(AssertionError) assertion error} and respond with a {@link MockTCPServer#getNAK() NAK}. * * @param expectedMessage a Regular Expression that describes what the next received message will be. */ public synchronized void setExpectedMessage(final String expectedMessage) { this.expectedMessage = new DataStreamRegexMatcher(expectedMessage); } /** * If any message, other that this one, is the next message to be received, record it as an {@link MockTCPServer#setAssertionError(AssertionError) assertion error} and respond with a {@link MockTCPServer#getNAK() NAK}. * * @param expectedMessage a Regular Expression that describes what the next received message will be. */ public synchronized void setExpectedMessage(final StringBuilder expectedMessage) { this.setExpectedMessage(expectedMessage.toString()); } /** * The number of messages received by the server since the server was started. * * @return The number of messages received by the server since the server was started. Default is 0. */ public int getMessagesReceivedCount() { return this.messagesReceivedCount; } /** * Add one to the number of messages received by the server since the server was started. see {@link MockTCPServer#getMessagesReceivedCount()} */ private void incrementMessagesReceivedCount() { this.messagesReceivedCount++; } /** * Close the socket (if it is open) and any open data streams. */ @Override public synchronized void close() { this.logger.info("Closing..."); if (this.getStatus() != Status.CLOSING) { this.setStatus(Status.CLOSED); } this.closeStreams(); while (super.isAlive() && this.getStatus() != Status.CLOSING) { final long maximumTimeToWait = 10000; try { super.join(maximumTimeToWait); } catch (final InterruptedException e) { Thread.currentThread().interrupt(); } // Intermittently, the server fails to close. Retry it indefinitely until it does close; an improvement of blocking forever with no feedback. if (super.isAlive()) { this.logger.warn(String.format("Failed to close the Server(%s) in %d milliseconds. Trying again to shutdown the Server...", super.getName(), maximumTimeToWait)); if (!super.isInterrupted()) { this.logger.trace(String.format("Interrupting the Server Thread(%s)...", super.getName())); this.closeStreams(); } } } this.logger.info("Closed."); } private void closeStreams() { try { if (this.getConnectionSocket() != null && !this.getConnectionSocket().isInputShutdown()) { this.getConnectionSocket().shutdownInput(); } if (this.getConnectionSocket() != null && !this.getConnectionSocket().isOutputShutdown()) { this.getConnectionSocket().shutdownOutput(); } } catch (final IOException e) { this.logger.error(e.getMessage(), e); } this.setInputStream(null); this.setOutputStream(null); // Do not set the ServerSocket to null; just close the Stream. this.logger.info("Closing the socket..."); IOUtils.closeQuietly(this.socket); this.logger.info("Closed the socket."); } private Status getStatus() { return this.status; } private synchronized void setStatus(final Status status) { this.status = status; } private int getPort() { try { if (this.port == null) { this.port = this.configurationSettings.getPort(); } } catch (final ConfigurationException e) { this.logger.error(e.getMessage(), e); } return this.port; } private void setPort(int port) { this.port = port; } /** * Open the Server Socket and wait for a connection. *

* The socket is opened on the configured {@link #getPort() port} on localhost. * * @return a new ServerSocket. * @throws IOException * @throws ConfigurationException */ private ServerSocket getSocket() throws IOException, ConfigurationException { if (this.socket == null || this.socket.isClosed()) { this.logger.info(String.format("Opening a socket on port %d...", this.getPort())); this.setSocket(new ServerSocket(this.getPort())); this.logger.info("Waiting for a connection..."); this.setConnectionSocket(this.socket.accept()); this.logger.info("Accepted a connection."); this.setInputStream(new BufferedReader(new InputStreamReader(this.getConnectionSocket().getInputStream()))); this.setOutputStream(new DataOutputStream(this.getConnectionSocket().getOutputStream())); this.logger.info("Ready to receive input."); } return this.socket; } private void setSocket(final ServerSocket socket) { this.socket = socket; } private Socket getConnectionSocket() { return this.connectionSocket; } private void setConnectionSocket(final Socket connectionSocket) { this.connectionSocket = connectionSocket; } private DataStream getDataStream() { if (this.dataStream == null) { this.dataStream = new DataStream(this.getTerminator().length, this.getRootLoggerName()); } return this.dataStream; } private void setDataStream(final DataStream dataStream) { this.logger.debug("Closing the DataStream..."); IOUtils.closeQuietly(this.dataStream); this.logger.debug("Closed the DataStream."); this.dataStream = dataStream; } private BufferedReader getInputStream() { return this.inputStream; } private void setInputStream(final BufferedReader inputStream) { this.logger.info("Closing input stream..."); IOUtils.closeQuietly(this.inputStream); this.logger.info("Closed input stream."); this.inputStream = inputStream; } private DataOutputStream getOutputStream() { return this.outputStream; } private void setOutputStream(final DataOutputStream outputStream) { this.logger.info("Closing the output stream..."); IOUtils.closeQuietly(this.outputStream); this.logger.info("Closed the output stream."); this.outputStream = outputStream; } private static Options getCommandLineOptions() { // create the Options final Options options = new Options(); options.addOption("p", "port", false, "use this port instead of the default one, or the one specified in the configuration file."); options.getOption("port").setArgs(1); options.addOption("h", "help", false, "print these usage instructions and exit."); options.addOption("?", "help", false, "print these usage instructions and exit."); options.addOption("v", "version", false, "print product version and exit."); return options; } /** * The log4j root logger name that will contain the class name, even if instantiated as an anonymous class. * * @return a root logger name. */ public String getRootLoggerName() { return this.getThreadName().replaceAll("-", "."); } /** * Derives a {@link Thread#getName() Thread name} that includes the class name, even if this object instantiated as an anonymous class. * * @return a value used as the log4j root logger and the Thread name. */ private String getThreadName() { final String delimeter = "."; final String regEx = "\\."; String name; if (StringUtils.isNotBlank(this.getClass().getSimpleName())) { name = this.getClass().getSimpleName(); } else { if (this.getClass().getName().contains(delimeter)) { final String[] nameSegments = this.getClass().getName().split(regEx); name = String.format("%s-%s", this.getClass().getSuperclass().getSimpleName(), nameSegments[nameSegments.length - 1]); } else { name = this.getClass().getName(); } } return name; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy