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 static org.junit.Assert.assertThat;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.DataOutputStream;
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 org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;

import io.cloudracer.datastream.DataStream;
import io.cloudracer.datastream.DataStreamRegexMatcher;

/**
 * A mock server used for testing purposes only.
 *
 * @author John McDonnell
 */
public class MockTCPServer extends Thread implements Closeable {

    private Logger logger = Logger.getLogger(getRootLoggerName());

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

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

    private AssertionError assertionError;

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

    private DataStream dataStream;

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

    public MockTCPServer(final int port) {
        logger.info("Starting...");

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

        setPort(port);
        start();
    }

    @Override
    public void run() {
        try {
            while (!getSocket().isClosed()) {
                while ((getDataStream().write(getInputStream().read())) != -1) {
                    if (Arrays.equals(getDataStream().getTail(), getTerminator())) {
                        incrementMessagesReceivedCount();

                        break;
                    }
                }
                // Ignore null in order allow a probing ping e.g. paping.exe
                if (getDataStream().size() > 0) {
                    setAssertionError(null);
                    try {
                        if (getExpectedMessage() != null) {
                            assertThat("Unexpected message from the AM Host Client.", getDataStream(), getExpectedMessage());
                        }
                    } catch (AssertionError e) {
                        setAssertionError(e);
                    }
                    onMessage(getDataStream());
                    byte[] response = null;
                    if (!getIsAlwaysNoResponse()) {
                        if (getAssertionError() == null && !getIsAlwaysNAKResponse()) {
                            response = getACK();
                        } else {
                            response = getNAK();
                        }

                        getOutputStream().write(response);

                        afterResponse(response);
                    }

                    setOutputStream(null);
                }
            }
        } catch (SocketException e) {
            logger.warn(e.getMessage());
        } catch (IOException e) {
            logger.error(e.getMessage(), e);
        }
    }

    /**
     * The server will read the stream until these characters are encountered.
     *
     * @return the terminator.
     */
    public byte[] getTerminator() {
        if (terminator == null) {
            terminator = DEFAULT_TERMINATOR;
        }

        return terminator;
    }

    /**
     * The server will read the stream until these characters are encountered.
     *
     * @param terminator
     *        the terminator.
     */
    public void setTerminator(byte[] terminator) {
        this.terminator = terminator;
    }

    /**
     * The positive acknowledgement response.
     *
     * @return positive acknowledgement
     */
    public byte[] getACK() {
        if (this.ack == null) {
            this.ack = DEFAULT_ACK;
        }

        return ack;
    }

    /**
     * The positive acknowledgement response.
     *
     * @param ack
     *        positive acknowledgement
     */
    public void setACK(byte[] ack) {
        this.ack = ack;
    }

    /**
     * The negative acknowledgement response.
     *
     * @return negative acknowledgement
     */
    public byte[] getNAK() {
        if (this.nak == null) {
            this.nak = DEFAULT_NAK;
        }

        return nak;
    }

    /**
     * The negative acknowledgement response.
     *
     * @param nak
     *        negative acknowledgement
     */
    public void setNAK(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.
     * @throws IOException
     */
    public void afterResponse(final byte[] response) throws IOException {
        logger.info(String.format("Sent the response: %s.", new String(response)));

        if (getIsCloseAfterNextResponse()) {
            close();
        }
    }

    /**
     * A server callback when a message is received.
     *
     * @param message
     *        the message received.
     */
    public void onMessage(final DataStream message) {
        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.
     */
    public void setAssertionError(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 isCloseAfterNextResponse * if true, the Servers next response will always be a NAK. */ public void setIsAlwaysNAKResponse(final boolean isAlwaysNAKResponse) { this.setIsAlwaysNAKResponse = isAlwaysNAKResponse; } public boolean getIsAlwaysNoResponse() { return setIsAlwaysNoResponse; } public 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. */ public boolean getIsCloseAfterNextResponse() { return 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. */ public void setIsCloseAfterNextResponse(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 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}. * * @param expectedMessage * a Regular Expression that describes what the next received * message will be. */ public 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}. * * @param expectedMessage * a Regular Expression that describes what the next received * message will be. */ public void setExpectedMessage(final StringBuffer expectedMessage) { setExpectedMessage(expectedMessage.toString()); } public int getMessagesReceivedCount() { return messagesReceivedCount; } private void incrementMessagesReceivedCount() { this.messagesReceivedCount++; } /** * Close the socket (if it is open) and any open data streams. * * @throws IOException */ @Override public void close() throws IOException { logger.info("Closing..."); setOutputStream(null); setInputStream(null); setSocket(null); } private int getPort() { return port; } private void setPort(int port) { this.port = port; } private ServerSocket getSocket() throws IOException { if (socket == null) { logger.info(String.format("Opening a socket on port %d...", getPort())); setSocket(new ServerSocket(getPort())); logger.info("Waiting for a connection..."); final Socket connectionSocket = socket.accept(); logger.info(String.format("Accepted a connection from %s.", socket.getLocalSocketAddress())); setInputStream(new BufferedReader(new InputStreamReader(connectionSocket.getInputStream()))); setOutputStream(new DataOutputStream(connectionSocket.getOutputStream())); logger.info("Ready to receive input."); } return socket; } private void setSocket(ServerSocket socket) { if (socket == null && this.socket != null) { try { logger.info("Closing the server socket..."); this.socket.close(); } catch (IOException e) { logger.error(e.getMessage(), e); } } this.socket = socket; } public DataStream getDataStream() { if (dataStream == null) { dataStream = new DataStream(getRootLoggerName(), getTerminator().length); } return dataStream; } public void setDataStream(DataStream dataStream) { this.dataStream = dataStream; } private BufferedReader getInputStream() { return inputStream; } private void setInputStream(BufferedReader inputStream) throws IOException { if (inputStream == null && this.inputStream != null) { logger.info("Closing the input stream..."); IOUtils.closeQuietly(this.inputStream); } this.inputStream = inputStream; } private DataOutputStream getOutputStream() { return outputStream; } private void setOutputStream(DataOutputStream outputStream) throws IOException { if (outputStream == null && this.outputStream != null) { logger.info("Closing the output stream..."); IOUtils.closeQuietly(this.outputStream); } this.outputStream = outputStream; } /** * 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 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 = null; 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 - 2025 Weber Informatics LLC | Privacy Policy