/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.camel.test.junit.rule.mllp; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.BindException; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.net.SocketTimeoutException; import java.util.LinkedList; import java.util.List; import java.util.regex.Pattern; import org.junit.rules.ExternalResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.apache.camel.component.mllp.MllpEndpoint.END_OF_BLOCK; import static org.apache.camel.component.mllp.MllpEndpoint.END_OF_DATA; import static org.apache.camel.component.mllp.MllpEndpoint.END_OF_STREAM; import static org.apache.camel.component.mllp.MllpEndpoint.MESSAGE_TERMINATOR; import static org.apache.camel.component.mllp.MllpEndpoint.SEGMENT_DELIMITER; import static org.apache.camel.component.mllp.MllpEndpoint.START_OF_BLOCK; /** * MLLP Test Server packaged as a JUnit Rule * * The server can be configured to simulate a large number * of error conditions. */ public class MllpServerResource extends ExternalResource { Logger log = LoggerFactory.getLogger(this.getClass()); String listenHost; int listenPort; int backlog = 5; int counter = 1; boolean active = true; int delayBeforeStartOfBlock; int delayBeforeAcknowledgement; int delayDuringAcknowledgement; int delayAfterAcknowledgement; int delayAfterEndOfBlock; int excludeStartOfBlockModulus; int excludeEndOfBlockModulus; int excludeEndOfDataModulus; int excludeAcknowledgementModulus; int sendOutOfBandDataModulus; int closeSocketBeforeAcknowledgementModulus; int closeSocketAfterAcknowledgementModulus; int resetSocketBeforeAcknowledgementModulus; int resetSocketAfterAcknowledgementModulus; int sendApplicationRejectAcknowledgementModulus; int sendApplicationErrorAcknowledgementModulus; Pattern sendApplicationRejectAcknowledgementPattern; Pattern sendApplicationErrorAcknowledgementPattern; String acknowledgementString; ServerSocketThread serverSocketThread; public MllpServerResource() { } public MllpServerResource(int listenPort) { this.listenPort = listenPort; } public MllpServerResource(int listenPort, int backlog) { this.listenPort = listenPort; this.backlog = backlog; } public MllpServerResource(String listenHost, int listenPort) { this.listenHost = listenHost; this.listenPort = listenPort; } public MllpServerResource(String listenHost, int listenPort, int backlog) { this.listenHost = listenHost; this.listenPort = listenPort; this.backlog = backlog; } public String getListenHost() { return listenHost; } public void setListenHost(String listenHost) { this.listenHost = listenHost; } public int getListenPort() { return listenPort; } public void setListenPort(int listenPort) { this.listenPort = listenPort; } public int getBacklog() { return backlog; } public void setBacklog(int backlog) { this.backlog = backlog; } public void startup() throws IOException { this.active = true; if (null != listenHost) { serverSocketThread = new ServerSocketThread(listenHost, listenPort, backlog); } else { serverSocketThread = new ServerSocketThread(listenPort, backlog); listenHost = serverSocketThread.getListenHost(); } if (0 >= listenPort) { listenPort = serverSocketThread.listenPort; } serverSocketThread.setDaemon(true); serverSocketThread.start(); } public void shutdown() { this.active = false; serverSocketThread.shutdown(); serverSocketThread = null; } @Override protected void before() throws Throwable { startup(); super.before(); } @Override protected void after() { super.after(); shutdown(); } public void interrupt() { serverSocketThread.interrupt(); } public int getDelayBeforeStartOfBlock() { return delayBeforeStartOfBlock; } public void setDelayBeforeStartOfBlock(int delayBeforeStartOfBlock) { this.delayBeforeStartOfBlock = delayBeforeStartOfBlock; } public int getDelayBeforeAcknowledgement() { return delayBeforeAcknowledgement; } public void setDelayBeforeAcknowledgement(int delayBeforeAcknowledgement) { this.delayBeforeAcknowledgement = delayBeforeAcknowledgement; } public int getDelayDuringAcknowledgement() { return delayDuringAcknowledgement; } public void setDelayDuringAcknowledgement(int delayDuringAcknowledgement) { this.delayDuringAcknowledgement = delayDuringAcknowledgement; } public int getDelayAfterAcknowledgement() { return delayAfterAcknowledgement; } public void setDelayAfterAcknowledgement(int delayAfterAcknowledgement) { this.delayAfterAcknowledgement = delayAfterAcknowledgement; } public int getDelayAfterEndOfBlock() { return delayAfterEndOfBlock; } public void setDelayAfterEndOfBlock(int delayAfterEndOfBlock) { this.delayAfterEndOfBlock = delayAfterEndOfBlock; } public boolean sendApplicationRejectAcknowledgement(String hl7Message) { return evaluatePattern(hl7Message, this.sendApplicationErrorAcknowledgementPattern); } public boolean sendApplicationErrorAcknowledgement(String hl7Message) { return evaluatePattern(hl7Message, this.sendApplicationRejectAcknowledgementPattern); } public boolean sendApplicationRejectAcknowledgement(int messageCount) { return evaluateModulus(messageCount, this.sendApplicationRejectAcknowledgementModulus); } public boolean sendApplicationErrorAcknowledgement(int messageCount) { return evaluateModulus(messageCount, this.sendApplicationErrorAcknowledgementModulus); } public boolean excludeStartOfBlock(int messageCount) { return evaluateModulus(messageCount, excludeStartOfBlockModulus); } public boolean excludeAcknowledgement(int messageCount) { return evaluateModulus(messageCount, excludeAcknowledgementModulus); } public boolean excludeEndOfBlock(int messageCount) { return evaluateModulus(messageCount, excludeEndOfBlockModulus); } public boolean excludeEndOfData(int messageCount) { return evaluateModulus(messageCount, excludeEndOfDataModulus); } public boolean closeSocketBeforeAcknowledgement(int messageCount) { return evaluateModulus(messageCount, closeSocketBeforeAcknowledgementModulus); } public boolean closeSocketAfterAcknowledgement(int messageCount) { return evaluateModulus(messageCount, closeSocketAfterAcknowledgementModulus); } public boolean resetSocketBeforeAcknowledgement(int messageCount) { return evaluateModulus(messageCount, resetSocketBeforeAcknowledgementModulus); } public boolean resetSocketAfterAcknowledgement(int messageCount) { return evaluateModulus(messageCount, resetSocketAfterAcknowledgementModulus); } public boolean sendOutOfBandData(int messageCount) { return evaluateModulus(messageCount, sendOutOfBandDataModulus); } private boolean evaluateModulus(int messageCount, int modulus) { switch (modulus) { case 0: return false; case 1: return true; default: return (messageCount % modulus == 0) ? true : false; } } private boolean evaluatePattern(String hl7Message, Pattern pattern) { boolean retValue = false; if (null != pattern && pattern.matcher(hl7Message).matches()) { retValue = true; } return retValue; } public boolean isActive() { return active; } public void setActive(boolean active) { this.active = active; } public int getExcludeStartOfBlockModulus() { return excludeStartOfBlockModulus; } /** * Set the modulus used to determine when to include the bMLLP_ENVELOPE_START_OF_BLOCK * portion of the MLLP Envelope. * <p/> * If this value is less than or equal to 0, the bMLLP_ENVELOPE_START_OF_BLOCK portion * of the MLLP Envelope will always be included. * If the value is 1, the bMLLP_ENVELOPE_START_OF_BLOCK portion of the MLLP Envelope will * never be included. * Otherwise, if the result of evaluating message count % value is greater * than 0, the bMLLP_ENVELOPE_START_OF_BLOCK portion of the MLLP Envelope will not be * included. Effectively leaving the bMLLP_ENVELOPE_START_OF_BLOCK portion of the MLLP Envelope * out of every n-th message. * * @param excludeStartOfBlockModulus exclude on every n-th message * 0 => Never excluded * 1 => Always excluded */ public void setExcludeStartOfBlockModulus(int excludeStartOfBlockModulus) { if (0 > excludeStartOfBlockModulus) { this.excludeStartOfBlockModulus = 0; } else { this.excludeStartOfBlockModulus = excludeStartOfBlockModulus; } } public void enableMllpEnvelope() { this.setExcludeStartOfBlockModulus(0); this.setExcludeEndOfBlockModulus(0); this.setExcludeEndOfDataModulus(0); } public void disableMllpEnvelopeStart() { this.disableMllpEnvelopeStart(1); } public void disableMllpEnvelopeStart(int mllpEnvelopeModulus) { this.setExcludeStartOfBlockModulus(mllpEnvelopeModulus); } public void disableMllpEnvelopeEnd() { this.disableMllpEnvelope(1); } public void disableMllpEnvelopeEnd(int mllpEnvelopeModulus) { this.setExcludeEndOfBlockModulus(mllpEnvelopeModulus); this.setExcludeEndOfDataModulus(mllpEnvelopeModulus); } public void disableMllpEnvelope() { this.disableMllpEnvelope(1); } public void disableMllpEnvelope(int mllpEnvelopeModulus) { this.setExcludeStartOfBlockModulus(mllpEnvelopeModulus); this.setExcludeEndOfBlockModulus(mllpEnvelopeModulus); this.setExcludeEndOfDataModulus(mllpEnvelopeModulus); } public void enableResponse() { this.setExcludeStartOfBlockModulus(0); this.setExcludeAcknowledgementModulus(0); this.setExcludeEndOfBlockModulus(0); this.setExcludeEndOfDataModulus(0); } public void disableResponse() { this.disableResponse(1); } public void disableResponse(int mllpResponseModulus) { this.setExcludeStartOfBlockModulus(mllpResponseModulus); this.setExcludeAcknowledgementModulus(mllpResponseModulus); this.setExcludeEndOfBlockModulus(mllpResponseModulus); this.setExcludeEndOfDataModulus(mllpResponseModulus); } public int getExcludeEndOfBlockModulus() { return excludeEndOfBlockModulus; } public void setExcludeEndOfBlockModulus(int excludeEndOfBlockModulus) { if (0 > excludeEndOfBlockModulus) { this.excludeEndOfBlockModulus = 0; } else { this.excludeEndOfBlockModulus = excludeEndOfBlockModulus; } } public int getExcludeEndOfDataModulus() { return excludeEndOfDataModulus; } public void setExcludeEndOfDataModulus(int excludeEndOfDataModulus) { if (0 > excludeEndOfDataModulus) { this.excludeEndOfDataModulus = 0; } else { this.excludeEndOfDataModulus = excludeEndOfDataModulus; } } public int getExcludeAcknowledgementModulus() { return excludeAcknowledgementModulus; } public void setExcludeAcknowledgementModulus(int excludeAcknowledgementModulus) { if (0 > excludeAcknowledgementModulus) { this.excludeAcknowledgementModulus = 0; } else { this.excludeAcknowledgementModulus = excludeAcknowledgementModulus; } } public int getSendOutOfBandDataModulus() { return sendOutOfBandDataModulus; } public void setSendOutOfBandDataModulus(int sendOutOfBandDataModulus) { if (0 > sendOutOfBandDataModulus) { this.sendOutOfBandDataModulus = 0; } else { this.sendOutOfBandDataModulus = sendOutOfBandDataModulus; } } public int getCloseSocketBeforeAcknowledgementModulus() { return closeSocketBeforeAcknowledgementModulus; } public void setCloseSocketBeforeAcknowledgementModulus(int closeSocketBeforeAcknowledgementModulus) { if (0 > closeSocketBeforeAcknowledgementModulus) { this.closeSocketBeforeAcknowledgementModulus = 0; } else { this.closeSocketBeforeAcknowledgementModulus = closeSocketBeforeAcknowledgementModulus; } } public int getCloseSocketAfterAcknowledgementModulus() { return closeSocketAfterAcknowledgementModulus; } public void setCloseSocketAfterAcknowledgementModulus(int closeSocketAfterAcknowledgementModulus) { if (0 > closeSocketAfterAcknowledgementModulus) { this.closeSocketAfterAcknowledgementModulus = 0; } else { this.closeSocketAfterAcknowledgementModulus = closeSocketAfterAcknowledgementModulus; } } public int getResetSocketBeforeAcknowledgementModulus() { return resetSocketBeforeAcknowledgementModulus; } public void setResetSocketBeforeAcknowledgementModulus(int resetSocketBeforeAcknowledgementModulus) { if (0 > resetSocketBeforeAcknowledgementModulus) { this.resetSocketBeforeAcknowledgementModulus = 0; } else { this.resetSocketBeforeAcknowledgementModulus = resetSocketBeforeAcknowledgementModulus; } } public int getResetSocketAfterAcknowledgementModulus() { return resetSocketAfterAcknowledgementModulus; } public void setResetSocketAfterAcknowledgementModulus(int resetSocketAfterAcknowledgementModulus) { if (0 > resetSocketAfterAcknowledgementModulus) { this.resetSocketAfterAcknowledgementModulus = 0; } else { this.resetSocketAfterAcknowledgementModulus = resetSocketAfterAcknowledgementModulus; } } public int getSendApplicationRejectAcknowledgementModulus() { return sendApplicationRejectAcknowledgementModulus; } public void setSendApplicationRejectAcknowledgementModulus(int sendApplicationRejectAcknowledgementModulus) { if (0 > sendApplicationRejectAcknowledgementModulus) { this.sendApplicationRejectAcknowledgementModulus = 0; } else { this.sendApplicationRejectAcknowledgementModulus = sendApplicationRejectAcknowledgementModulus; } } public int getSendApplicationErrorAcknowledgementModulus() { return sendApplicationErrorAcknowledgementModulus; } public void setSendApplicationErrorAcknowledgementModulus(int sendApplicationErrorAcknowledgementModulus) { if (0 > sendApplicationErrorAcknowledgementModulus) { this.sendApplicationErrorAcknowledgementModulus = 0; } else { this.sendApplicationErrorAcknowledgementModulus = sendApplicationErrorAcknowledgementModulus; } } public Pattern getSendApplicationRejectAcknowledgementPattern() { return sendApplicationRejectAcknowledgementPattern; } public void setSendApplicationRejectAcknowledgementPattern(Pattern sendApplicationRejectAcknowledgementPattern) { this.sendApplicationRejectAcknowledgementPattern = sendApplicationRejectAcknowledgementPattern; } public Pattern getSendApplicationErrorAcknowledgementPattern() { return sendApplicationErrorAcknowledgementPattern; } public void setSendApplicationErrorAcknowledgementPattern(Pattern sendApplicationErrorAcknowledgementPattern) { this.sendApplicationErrorAcknowledgementPattern = sendApplicationErrorAcknowledgementPattern; } public String getAcknowledgementString() { return acknowledgementString; } public void setAcknowledgementString(String acknowledgementString) { this.acknowledgementString = acknowledgementString; } public ServerSocketThread getServerSocketThread() { return serverSocketThread; } public void setServerSocketThread(ServerSocketThread serverSocketThread) { this.serverSocketThread = serverSocketThread; } public void closeClientConnections() { if (serverSocketThread != null) { serverSocketThread.closeClientConnections(); } } public void resetClientConnections() { if (serverSocketThread != null) { serverSocketThread.resetClientConnections(); } } /** * Generates a HL7 Application Acknowledgement * * @param hl7Message HL7 message that is being acknowledged * @param acknowledgementCode AA, AE or AR * @return a HL7 Application Acknowledgement */ protected String generateAcknowledgement(String hl7Message, String acknowledgementCode) { final String defaulNackMessage = "MSH|^~\\&|||||||NACK||P|2.2" + SEGMENT_DELIMITER + "MSA|AR|" + SEGMENT_DELIMITER + MESSAGE_TERMINATOR; if (hl7Message == null) { log.error("Invalid HL7 message for parsing operation. Please check your inputs"); return defaulNackMessage; } if (!("AA".equals(acknowledgementCode) || "AE".equals(acknowledgementCode) || "AR".equals(acknowledgementCode))) { throw new IllegalArgumentException("Acknowledgemnt Code must be AA, AE or AR: " + acknowledgementCode); } String messageControlId; int endOfMshSegment = hl7Message.indexOf(SEGMENT_DELIMITER); if (-1 != endOfMshSegment) { String mshSegment = hl7Message.substring(0, endOfMshSegment); char fieldSeparator = mshSegment.charAt(3); String fieldSeparatorPattern = Pattern.quote("" + fieldSeparator); String[] mshFields = mshSegment.split(fieldSeparatorPattern); if (mshFields.length == 0) { log.error("Failed to split MSH Segment into fields"); } else { StringBuilder ackBuilder = new StringBuilder(mshSegment.length() + 25); // Build the MSH Segment ackBuilder .append(mshFields[0]).append(fieldSeparator) .append(mshFields[1]).append(fieldSeparator) .append(mshFields[4]).append(fieldSeparator) .append(mshFields[5]).append(fieldSeparator) .append(mshFields[2]).append(fieldSeparator) .append(mshFields[3]).append(fieldSeparator) .append(mshFields[6]).append(fieldSeparator) .append(mshFields[7]).append(fieldSeparator) .append("ACK") .append(mshFields[8].substring(3)); for (int i = 9; i < mshFields.length; ++i) { ackBuilder.append(fieldSeparator).append(mshFields[i]); } // Empty fields at the end are not preserved by String.split, so preserve them int emptyFieldIndex = mshSegment.length() - 1; if (fieldSeparator == mshSegment.charAt(mshSegment.length() - 1)) { ackBuilder.append(fieldSeparator); while (emptyFieldIndex >= 1 && mshSegment.charAt(emptyFieldIndex) == mshSegment.charAt(emptyFieldIndex - 1)) { ackBuilder.append(fieldSeparator); --emptyFieldIndex; } } ackBuilder.append(SEGMENT_DELIMITER); // Build the MSA Segment ackBuilder .append("MSA").append(fieldSeparator) .append(acknowledgementCode).append(fieldSeparator) .append(mshFields[9]).append(fieldSeparator) .append(SEGMENT_DELIMITER); // Terminate the message ackBuilder.append(MESSAGE_TERMINATOR); return ackBuilder.toString(); } } else { log.error("Failed to find the end of the MSH Segment"); } return null; } /** * Nested class to accept TCP connections */ class ServerSocketThread extends Thread { Logger log = LoggerFactory.getLogger(this.getClass()); final long bindTimeout = 30000; final long bindRetryDelay = 1000; ServerSocket serverSocket; List<ClientSocketThread> clientSocketThreads = new LinkedList<>(); String listenHost; int listenPort; int backlog = 5; int acceptTimeout = 5000; boolean raiseExceptionOnAcceptTimeout; ServerSocketThread() throws IOException { bind(); } ServerSocketThread(int listenPort) throws IOException { this.listenPort = listenPort; bind(); } ServerSocketThread(int listenPort, int backlog) throws IOException { this.listenPort = listenPort; this.backlog = backlog; bind(); } ServerSocketThread(String listenHost, int listenPort, int backlog) throws IOException { this.listenHost = listenHost; this.listenPort = listenPort; this.backlog = backlog; bind(); } /** * Open the TCP Listener * * @throws IOException */ private void bind() throws IOException { this.setDaemon(true); serverSocket = new ServerSocket(); // Set TCP Parameters serverSocket.setSoTimeout(acceptTimeout); serverSocket.setReuseAddress(true); InetSocketAddress listenAddress; if (null != this.listenHost) { listenAddress = new InetSocketAddress(this.listenHost, this.listenPort); } else { listenAddress = new InetSocketAddress(this.listenPort); } long startTicks = System.currentTimeMillis(); while (!serverSocket.isBound()) { try { serverSocket.bind(listenAddress, backlog); } catch (BindException bindEx) { if (System.currentTimeMillis() < startTicks + bindTimeout) { log.warn("Unable to bind to {} - retrying in {} milliseconds", listenAddress.toString(), bindRetryDelay); try { Thread.sleep(bindRetryDelay); } catch (InterruptedException interruptedEx) { log.error("Wait for bind retry was interrupted - rethrowing BindException"); throw bindEx; } } } } if (0 >= this.listenPort) { this.listenPort = serverSocket.getLocalPort(); } log.info("Opened TCP Listener on port {}", serverSocket.getLocalPort()); } void closeClientConnections() { if (clientSocketThreads != null) { for (ClientSocketThread clientSocketThread : clientSocketThreads) { clientSocketThread.closeConnection(); } } } void resetClientConnections() { if (clientSocketThreads != null) { for (ClientSocketThread clientSocketThread : clientSocketThreads) { clientSocketThread.resetConnection(); } } } /** * Accept TCP connections and create ClientSocketThreads for them */ public void run() { log.info("Accepting connections on port {}", serverSocket.getLocalPort()); this.setName("MllpServerResource$ServerSocketThread - " + serverSocket.getLocalSocketAddress().toString()); while (!isInterrupted() && serverSocket.isBound() && !serverSocket.isClosed()) { Socket clientSocket = null; try { clientSocket = serverSocket.accept(); } catch (SocketTimeoutException timeoutEx) { if (raiseExceptionOnAcceptTimeout) { throw new MllpJUnitResourceTimeoutException("Timeout Accepting client connection", timeoutEx); } log.warn("Timeout waiting for client connection"); } catch (SocketException socketEx) { log.debug("SocketException encountered accepting client connection - ignoring", socketEx); if (null == clientSocket) { continue; } else if (!clientSocket.isClosed()) { try { clientSocket.setSoLinger(true, 0); } catch (SocketException soLingerEx) { log.warn("Ignoring SocketException encountered when setting SO_LINGER in preparation of resetting client Socket", soLingerEx); } try { clientSocket.close(); } catch (IOException ioEx) { log.warn("Ignoring IOException encountered when resetting client Socket", ioEx); } continue; } else { throw new MllpJUnitResourceException("Unexpected SocketException encountered accepting client connection", socketEx); } } catch (Exception ex) { throw new MllpJUnitResourceException("Unexpected exception encountered accepting client connection", ex); } if (null != clientSocket) { try { clientSocket.setKeepAlive(true); clientSocket.setTcpNoDelay(false); clientSocket.setSoLinger(false, -1); clientSocket.setSoTimeout(5000); ClientSocketThread clientSocketThread = new ClientSocketThread(clientSocket); clientSocketThread.setDaemon(true); clientSocketThread.start(); clientSocketThreads.add(clientSocketThread); } catch (Exception unexpectedEx) { log.warn("Unexpected exception encountered configuring client socket"); try { clientSocket.close(); } catch (IOException ingoreEx) { log.warn("Exceptiong encountered closing client socket after attempting to accept connection", ingoreEx); } throw new MllpJUnitResourceException("Unexpected exception encountered configuring client socket", unexpectedEx); } } } log.info("No longer accepting connections - closing TCP Listener on port {}", serverSocket.getLocalPort()); try { serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } log.info("Closed TCP Listener on port {}", serverSocket.getLocalPort()); } public void shutdown() { this.interrupt(); } public String getListenHost() { return listenHost; } public int getListenPort() { return listenPort; } public int getBacklog() { return backlog; } public int getAcceptTimeout() { return acceptTimeout; } /** * Enable/disable a timeout while waiting for a TCP connection, in milliseconds. With this option set to a * non-zero timeout, the ServerSocketThread will block for only this amount of time while waiting for a tcp * connection. If the timeout expires and raiseExceptionOnAcceptTimeout is set to true, a MllpJUnitResourceTimeoutException * is raised. Otherwise, the ServerSocketThread will continue to poll for new TCP connections. * * @param acceptTimeout the timeout in milliseconds - zero is interpreted as an infinite timeout */ public void setAcceptTimeout(int acceptTimeout) { this.acceptTimeout = acceptTimeout; } public boolean isRaiseExceptionOnAcceptTimeout() { return raiseExceptionOnAcceptTimeout; } /** * Enable/Disable the generation of MllpJUnitResourceTimeoutException if the ServerSocket.accept() * call raises a SocketTimeoutException. * * @param raiseExceptionOnAcceptTimeout true enables exceptions on an accept timeout */ public void setRaiseExceptionOnAcceptTimeout(boolean raiseExceptionOnAcceptTimeout) { this.raiseExceptionOnAcceptTimeout = raiseExceptionOnAcceptTimeout; } @Override public void interrupt() { for (ClientSocketThread clientSocketThread : clientSocketThreads) { clientSocketThread.interrupt(); } if (serverSocket != null && serverSocket.isBound() && !serverSocket.isClosed()) { try { serverSocket.close(); } catch (Exception ex) { log.warn("Exception encountered closing server socket on interrupt", ex); } } super.interrupt(); } public void close() { } } /** * Nested class that handles the established TCP connections */ class ClientSocketThread extends Thread { /* final char cCARRIAGE_RETURN = 13; final char cLINE_FEED = 10; final char cSEGMENT_DELIMITER = cCARRIAGE_RETURN; final String sMESSAGE_TERMINATOR = "" + cCARRIAGE_RETURN + cLINE_FEED; final byte bMLLP_ENVELOPE_START_OF_BLOCK = 0x0b; final byte bMLLP_ENVELOPE_END_OF_BLOCK = 0x1c; final byte bMLLP_ENVELOPE_END_OF_DATA = 0x0d; final int iEND_OF_TRANSMISSION = -1; */ Logger log = LoggerFactory.getLogger(this.getClass()); Socket clientSocket; int messageCounter; ClientSocketThread(Socket clientSocket) { this.clientSocket = clientSocket; } void closeConnection() { if (clientSocket != null && !clientSocket.isClosed()) { try { clientSocket.close(); } catch (IOException ioEx) { log.warn("Ignoring IOException encountered when closing client Socket", ioEx); } finally { clientSocket = null; } } } void resetConnection() { if (clientSocket != null && !clientSocket.isClosed()) { try { clientSocket.setSoLinger(true, 0); } catch (SocketException socketEx) { log.warn("Ignoring SocketException encountered when setting SO_LINGER in preparation of resetting client Socket", socketEx); } try { clientSocket.close(); } catch (IOException ioEx) { log.warn("Ignoring IOException encountered when resetting client Socket", ioEx); } finally { clientSocket = null; } } } /** * Receives HL7 messages and replies with HL7 Acknowledgements. * * The exact behaviour of this method is very configurable, allowing simulation of varies * error conditions. */ public void run() { String localAddress = clientSocket.getLocalAddress().toString(); String remoteAddress = clientSocket.getRemoteSocketAddress().toString(); log.info("Handling Connection: {} -> {}", localAddress, remoteAddress); try { while (!isInterrupted() && null != clientSocket && clientSocket.isConnected() && !clientSocket.isClosed()) { InputStream instream; try { instream = clientSocket.getInputStream(); } catch (IOException ioEx) { if (clientSocket.isClosed()) { log.debug("Client socket was closed - ignoring exception", clientSocket); break; } else { throw new MllpJUnitResourceException("Unexpected IOException encounted getting input stream", ioEx); } } catch (Exception unexpectedEx) { throw new MllpJUnitResourceException("Unexpected exception encounted getting input stream", unexpectedEx); } String parsedHL7Message; try { parsedHL7Message = getMessage(instream); } catch (SocketTimeoutException timeoutEx) { log.info("Waiting for message from client"); continue; } if (null != parsedHL7Message && parsedHL7Message.length() > 0) { ++messageCounter; if (closeSocketBeforeAcknowledgement(messageCounter)) { log.warn("Closing socket before sending acknowledgement"); clientSocket.shutdownInput(); clientSocket.shutdownOutput(); clientSocket.close(); break; } if (resetSocketBeforeAcknowledgement(messageCounter)) { log.warn("Resetting socket before sending acknowledgement"); try { clientSocket.setSoLinger(true, 0); } catch (IOException ioEx) { log.warn("Ignoring IOException encountered setting SO_LINGER when prepareing to reset socket", ioEx); } clientSocket.shutdownInput(); clientSocket.shutdownOutput(); clientSocket.close(); break; } String acknowledgmentMessage; if (acknowledgementString == null) { if (sendApplicationErrorAcknowledgement(messageCounter) || sendApplicationErrorAcknowledgement(parsedHL7Message)) { acknowledgmentMessage = generateAcknowledgementMessage(parsedHL7Message, "AE"); } else if (sendApplicationRejectAcknowledgement(messageCounter) || sendApplicationRejectAcknowledgement(parsedHL7Message)) { acknowledgmentMessage = generateAcknowledgementMessage(parsedHL7Message, "AR"); } else { acknowledgmentMessage = generateAcknowledgementMessage(parsedHL7Message); } } else { acknowledgmentMessage = acknowledgementString; } BufferedOutputStream outstream = new BufferedOutputStream(clientSocket.getOutputStream()); if (sendOutOfBandData(messageCounter)) { byte[] outOfBandDataBytes = "Out Of Band Hl7MessageGenerator".getBytes(); outstream.write(outOfBandDataBytes, 0, outOfBandDataBytes.length); } if (excludeStartOfBlock(messageCounter)) { log.warn("NOT sending START_OF_BLOCK"); } else { outstream.write(START_OF_BLOCK); if (delayBeforeStartOfBlock > 0) { uncheckedSleep(delayBeforeStartOfBlock); uncheckedFlush(outstream); } } if (excludeAcknowledgement(messageCounter)) { log.info("NOT sending Acknowledgement body"); } else { if (delayBeforeAcknowledgement > 0) { uncheckedSleep(delayBeforeAcknowledgement); } log.debug("Buffering Acknowledgement\n\t{}", acknowledgmentMessage.replace('\r', '\n')); byte[] ackBytes = acknowledgmentMessage.getBytes(); if (delayDuringAcknowledgement > 0) { int firstHalf = ackBytes.length / 2; outstream.write(ackBytes, 0, firstHalf); uncheckedFlush(outstream); uncheckedSleep(delayDuringAcknowledgement); outstream.write(ackBytes, firstHalf, ackBytes.length - firstHalf); uncheckedFlush(outstream); } else { outstream.write(ackBytes, 0, ackBytes.length); } if (delayAfterAcknowledgement > 0) { uncheckedFlush(outstream); uncheckedSleep(delayAfterAcknowledgement); } } if (excludeEndOfBlock(messageCounter)) { log.warn("NOT sending bMLLP_ENVELOPE_END_OF_BLOCK"); } else { outstream.write(END_OF_BLOCK); if (delayAfterEndOfBlock > 0) { uncheckedFlush(outstream); uncheckedSleep(delayAfterEndOfBlock); } } if (excludeEndOfData(messageCounter)) { log.warn("NOT sending bMLLP_ENVELOPE_END_OF_DATA"); } else { outstream.write(END_OF_DATA); } log.debug("Writing Acknowledgement\n\t{}", acknowledgmentMessage.replace('\r', '\n')); uncheckedFlush(outstream); if (closeSocketAfterAcknowledgement(messageCounter)) { log.info("Closing Client"); clientSocket.shutdownInput(); clientSocket.shutdownOutput(); clientSocket.close(); break; } } } } catch (IOException e) { String errorMessage = "Error while reading and writing from clientSocket"; log.error(errorMessage, e); throw new MllpJUnitResourceException(errorMessage, e); } finally { if (clientSocket != null) { try { clientSocket.close(); } catch (IOException e) { String errorMessage = "Error while attempting to close to client Socket"; log.error(errorMessage, e); throw new MllpJUnitResourceException(errorMessage, e); } } } log.debug("Client Connection Finished: {} -> {}", localAddress, remoteAddress); } /** * Read a MLLP-Framed message * * @param anInputStream source input stream * @return the MLLP payload * @throws IOException when the underlying Java Socket calls raise these exceptions */ public String getMessage(InputStream anInputStream) throws IOException { try { boolean waitingForStartOfBlock = true; while (waitingForStartOfBlock) { int potentialStartCharacter = anInputStream.read(); switch (potentialStartCharacter) { case END_OF_STREAM: return null; case START_OF_BLOCK: waitingForStartOfBlock = false; break; default: log.warn("START_OF_BLOCK character has not been received. Out-of-band character received: {}", potentialStartCharacter); } } } catch (SocketException socketEx) { if (clientSocket != null) { if (clientSocket.isClosed()) { log.info("Client socket closed while waiting for MLLP_ENVELOPE_START_OF_BLOCK"); } else if (clientSocket.isConnected()) { log.info("SocketException encountered while waiting for MLLP_ENVELOPE_START_OF_BLOCK"); resetConnection(); } else { log.error("Unable to read from socket stream when expected bMLLP_ENVELOPE_START_OF_BLOCK - resetting connection ", socketEx); resetConnection(); } } return null; } boolean endOfMessage = false; StringBuilder parsedMessage = new StringBuilder(anInputStream.available() + 10); while (!endOfMessage) { int characterReceived = anInputStream.read(); switch (characterReceived) { case START_OF_BLOCK: log.error("Received START_OF_BLOCK before END_OF_DATA. Discarding data: {}", parsedMessage.toString()); return null; case END_OF_STREAM: log.error("Received END_OF_STREAM without END_OF_DATA. Discarding data: {}", parsedMessage.toString()); return null; case END_OF_BLOCK: characterReceived = anInputStream.read(); if (characterReceived != END_OF_DATA) { log.error("Received {} when expecting END_OF_DATA after END_OF_BLOCK. Discarding Hl7MessageGenerator: {}", characterReceived, parsedMessage.toString()); return null; } endOfMessage = true; break; default: parsedMessage.append((char) characterReceived); } } return parsedMessage.toString(); } /** * Generates a HL7 Application Accept Acknowledgement * * @param hl7Message HL7 message that is being acknowledged * @return a HL7 Application Accept Acknowlegdement */ private String generateAcknowledgementMessage(String hl7Message) { return generateAcknowledgementMessage(hl7Message, "AA"); } /** * Generates a HL7 Application Acknowledgement * * @param hl7Message HL7 message that is being acknowledged * @param acknowledgementCode AA, AE or AR * @return a HL7 Application Acknowledgement */ private String generateAcknowledgementMessage(String hl7Message, String acknowledgementCode) { return generateAcknowledgement(hl7Message, acknowledgementCode); } @Override public void interrupt() { if (clientSocket != null && clientSocket.isConnected() && !clientSocket.isClosed()) { try { clientSocket.close(); } catch (Exception ex) { log.warn("Exception encountered closing client socket on interrput", ex); } } super.interrupt(); } private void uncheckedSleep(long milliseconds) { try { Thread.sleep(milliseconds); } catch (InterruptedException e) { log.warn("Sleep interrupted", e); } } private void uncheckedFlush(OutputStream outputStream) { try { outputStream.flush(); } catch (IOException e) { log.warn("Ignoring exception caught while flushing output stream", e); } } } }