/** * 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.InetSocketAddress; import java.net.Socket; import java.net.SocketException; import java.net.SocketTimeoutException; import org.junit.rules.ExternalResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * MLLP Test Client packaged as a JUnit Rule * * The client can be configured to simulate a large number * of error conditions. */ public class MllpClientResource extends ExternalResource { static final char START_OF_BLOCK = 0x0b; static final char END_OF_BLOCK = 0x1c; static final char END_OF_DATA = 0x0d; static final int END_OF_STREAM = -1; Logger log = LoggerFactory.getLogger(this.getClass()); Socket clientSocket; InputStream inputStream; OutputStream outputStream; String mllpHost = "0.0.0.0"; int mllpPort = -1; boolean sendStartOfBlock = true; boolean sendEndOfBlock = true; boolean sendEndOfData = true; int connectTimeout = 5000; int soTimeout = 5000; boolean reuseAddress; boolean tcpNoDelay = true; DisconnectMethod disconnectMethod = DisconnectMethod.CLOSE; /** * Use this constructor to avoid having the connection started by JUnit (since the port is still -1) */ public MllpClientResource() { } public MllpClientResource(int port) { this.mllpPort = port; } public MllpClientResource(String host, int port) { this.mllpHost = host; this.mllpPort = port; } @Override protected void before() throws Throwable { if (0 < mllpPort) { this.connect(); } super.before(); } @Override protected void after() { super.after(); this.close(); } public void close() { try { if (null != inputStream) { clientSocket.close(); } } catch (IOException e) { log.warn(String.format("Exception encountered closing connection to {}:{}", mllpHost, mllpPort), e); } finally { inputStream = null; outputStream = null; clientSocket = null; } return; } public void connect() { this.connect(this.connectTimeout); } public void connect(int connectTimeout) { try { clientSocket = new Socket(); clientSocket.connect(new InetSocketAddress(mllpHost, mllpPort), connectTimeout); clientSocket.setSoTimeout(soTimeout); clientSocket.setSoLinger(false, -1); clientSocket.setReuseAddress(reuseAddress); clientSocket.setTcpNoDelay(tcpNoDelay); inputStream = clientSocket.getInputStream(); outputStream = new BufferedOutputStream(clientSocket.getOutputStream(), 2048); } catch (IOException e) { String errorMessage = String.format("Unable to establish connection to %s:%s", mllpHost, mllpPort); log.error(errorMessage, e); throw new MllpJUnitResourceException(errorMessage, e); } } public void reset() { try { clientSocket.setSoLinger(true, 0); } catch (SocketException socketEx) { log.warn("Exception encountered setting set SO_LINGER to force a TCP reset", socketEx); } try { if (null != inputStream) { clientSocket.close(); } } catch (IOException e) { log.warn(String.format("Exception encountered resetting connection to {}:{}", mllpHost, mllpPort), e); } finally { inputStream = null; outputStream = null; clientSocket = null; } return; } public void disconnect() { if (DisconnectMethod.RESET == disconnectMethod) { reset(); } else { close(); } } public DisconnectMethod getDisconnectMethod() { return disconnectMethod; } public boolean isConnected() { return clientSocket.isConnected() && !clientSocket.isClosed(); } public void sendData(String data) { boolean disconnectAfterSend = false; this.sendData(data, disconnectAfterSend); } public void sendData(String data, boolean disconnectAfterSend) { byte[] payloadBytes = data.getBytes(); try { outputStream.write(payloadBytes, 0, payloadBytes.length); } catch (IOException e) { log.error("Unable to send raw string", e); throw new MllpJUnitResourceException("Unable to send raw string", e); } if (disconnectAfterSend) { log.warn("Closing TCP connection"); disconnect(); } } public void sendFramedData(String hl7Message) { boolean disconnectAfterSend = false; this.sendFramedData(hl7Message, disconnectAfterSend); } public void sendFramedData(String hl7Message, boolean disconnectAfterSend) { if (null == clientSocket) { this.connect(); } if (!clientSocket.isConnected()) { throw new MllpJUnitResourceException("Cannot send message - client is not connected"); } if (null == outputStream) { throw new MllpJUnitResourceException("Cannot send message - output stream is null"); } byte[] payloadBytes = hl7Message.getBytes(); try { if (sendStartOfBlock) { outputStream.write(START_OF_BLOCK); } else { log.warn("Not sending START_OF_BLOCK"); } outputStream.write(payloadBytes, 0, payloadBytes.length); if (sendEndOfBlock) { outputStream.write(END_OF_BLOCK); } else { log.warn("Not sending END_OF_BLOCK"); } if (sendEndOfData) { outputStream.write(END_OF_DATA); } else { log.warn("Not sending END_OF_DATA"); } outputStream.flush(); } catch (IOException e) { log.error("Unable to send HL7 message", e); throw new MllpJUnitResourceException("Unable to send HL7 message", e); } if (disconnectAfterSend) { log.warn("Closing TCP connection"); disconnect(); } } public void sendFramedDataInMultiplePackets(String hl7Message, byte flushByte) { sendFramedDataInMultiplePackets(hl7Message, flushByte, false); } public void sendFramedDataInMultiplePackets(String hl7Message, byte flushByte, boolean disconnectAfterSend) { if (null == clientSocket) { this.connect(); } if (!clientSocket.isConnected()) { throw new MllpJUnitResourceException("Cannot send message - client is not connected"); } if (null == outputStream) { throw new MllpJUnitResourceException("Cannot send message - output stream is null"); } byte[] payloadBytes = hl7Message.getBytes(); try { if (sendStartOfBlock) { outputStream.write(START_OF_BLOCK); } else { log.warn("Not sending START_OF_BLOCK"); } for (byte payloadByte : payloadBytes) { outputStream.write(payloadByte); if (flushByte == payloadByte) { outputStream.flush(); } } if (sendEndOfBlock) { outputStream.write(END_OF_BLOCK); } else { log.warn("Not sending END_OF_BLOCK"); } if (sendEndOfData) { outputStream.write(END_OF_DATA); } else { log.warn("Not sending END_OF_DATA"); } outputStream.flush(); } catch (IOException e) { log.error("Unable to send HL7 message", e); throw new MllpJUnitResourceException("Unable to send HL7 message", e); } if (disconnectAfterSend) { log.warn("Closing TCP connection"); disconnect(); } } public String receiveFramedData() throws SocketException, SocketTimeoutException { return receiveFramedData(soTimeout); } public String receiveFramedData(int timout) throws SocketException, SocketTimeoutException { if (!isConnected()) { throw new MllpJUnitResourceException("Cannot receive acknowledgement - client is not connected"); } if (null == outputStream) { throw new MllpJUnitResourceException("Cannot receive acknowledgement - output stream is null"); } clientSocket.setSoTimeout(timout); StringBuilder acknowledgement = new StringBuilder(); try { int firstByte = inputStream.read(); if (START_OF_BLOCK != firstByte) { if (isConnected()) { if (END_OF_STREAM == firstByte) { log.warn("END_OF_STREAM reached while waiting for START_OF_BLOCK - closing socket"); try { clientSocket.close(); } catch (Exception ex) { log.warn("Exception encountered closing socket after receiving END_OF_STREAM while waiting for START_OF_BLOCK"); } return ""; } else { log.error("Acknowledgement did not start with START_OF_BLOCK: {}", firstByte); throw new MllpJUnitResourceCorruptFrameException("Message did not start with START_OF_BLOCK"); } } else { throw new MllpJUnitResourceException("Connection terminated"); } } boolean readingMessage = true; while (readingMessage) { int nextByte = inputStream.read(); switch (nextByte) { case -1: throw new MllpJUnitResourceCorruptFrameException("Reached end of stream before END_OF_BLOCK"); case START_OF_BLOCK: throw new MllpJUnitResourceCorruptFrameException("Received START_OF_BLOCK before END_OF_BLOCK"); case END_OF_BLOCK: if (END_OF_DATA != inputStream.read()) { throw new MllpJUnitResourceCorruptFrameException("END_OF_BLOCK was not followed by END_OF_DATA"); } readingMessage = false; break; default: acknowledgement.append((char) nextByte); } } } catch (SocketTimeoutException timeoutEx) { if (0 < acknowledgement.length()) { log.error("Timeout waiting for acknowledgement", timeoutEx); } else { log.error("Timeout while reading acknowledgement\n" + acknowledgement.toString().replace('\r', '\n'), timeoutEx); } throw new MllpJUnitResourceTimeoutException("Timeout while reading acknowledgement", timeoutEx); } catch (IOException e) { log.error("Unable to read HL7 acknowledgement", e); throw new MllpJUnitResourceException("Unable to read HL7 acknowledgement", e); } return acknowledgement.toString(); } public String receiveData() throws SocketException, SocketTimeoutException { return receiveData(soTimeout); } public String receiveData(int timeout) throws SocketException, SocketTimeoutException { clientSocket.setSoTimeout(timeout); StringBuilder availableInput = new StringBuilder(); try { do { availableInput.append((char) inputStream.read()); } while (0 < inputStream.available()); } catch (SocketTimeoutException timeoutEx) { log.error("Timeout while receiving available input", timeoutEx); throw new MllpJUnitResourceTimeoutException("Timeout while receiving available input", timeoutEx); } catch (IOException e) { log.warn("Exception encountered eating available input", e); throw new MllpJUnitResourceException("Exception encountered eating available input", e); } return availableInput.toString(); } public String eatData() throws SocketException, SocketTimeoutException { return eatData(soTimeout); } public String eatData(int timeout) throws SocketException { clientSocket.setSoTimeout(timeout); StringBuilder availableInput = new StringBuilder(); try { while (0 < inputStream.available()) { availableInput.append((char) inputStream.read()); } } catch (IOException e) { log.warn("Exception encountered eating available input", e); throw new MllpJUnitResourceException("Exception encountered eating available input", e); } return availableInput.toString(); } public String sendMessageAndWaitForAcknowledgement(String hl7Data) throws SocketException, SocketTimeoutException { sendFramedData(hl7Data); return receiveFramedData(); } public String sendMessageAndWaitForAcknowledgement(String hl7Data, int acknwoledgementTimeout) throws SocketException, SocketTimeoutException { sendFramedData(hl7Data); return receiveFramedData(acknwoledgementTimeout); } public String getMllpHost() { return mllpHost; } public void setMllpHost(String mllpHost) { this.mllpHost = mllpHost; } public int getMllpPort() { return mllpPort; } public void setMllpPort(int mllpPort) { this.mllpPort = mllpPort; } public boolean isSendStartOfBlock() { return sendStartOfBlock; } public void setSendStartOfBlock(boolean sendStartOfBlock) { this.sendStartOfBlock = sendStartOfBlock; } public boolean isSendEndOfBlock() { return sendEndOfBlock; } public void setSendEndOfBlock(boolean sendEndOfBlock) { this.sendEndOfBlock = sendEndOfBlock; } public boolean isSendEndOfData() { return sendEndOfData; } public void setSendEndOfData(boolean sendEndOfData) { this.sendEndOfData = sendEndOfData; } public int getConnectTimeout() { return connectTimeout; } public void setConnectTimeout(int connectTimeout) { this.connectTimeout = connectTimeout; } public int getSoTimeout() { return soTimeout; } public void setSoTimeout(int soTimeout) { this.soTimeout = soTimeout; } public boolean isReuseAddress() { return reuseAddress; } public void setReuseAddress(boolean reuseAddress) { this.reuseAddress = reuseAddress; } public boolean isTcpNoDelay() { return tcpNoDelay; } public void setTcpNoDelay(boolean tcpNoDelay) { this.tcpNoDelay = tcpNoDelay; } public void setDisconnectMethod(DisconnectMethod disconnectMethod) { this.disconnectMethod = disconnectMethod; } public enum DisconnectMethod { CLOSE, RESET } }