org.apache.camel.component.mllp.internal.Hl7Util Maven / Gradle / Ivy
/*
* 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.component.mllp.internal;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.camel.component.mllp.MllpAcknowledgementGenerationException;
import org.apache.camel.component.mllp.MllpProtocolConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class Hl7Util {
public static final String NULL_REPLACEMENT_VALUE = "";
public static final String EMPTY_REPLACEMENT_VALUE = "<>";
public static final Map CHARACTER_REPLACEMENTS;
public static final SimpleDateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("yyyyMMddHHmmss.SSSZZZZ");
static final int STRING_BUFFER_PAD_SIZE = 100;
static final Logger LOG = LoggerFactory.getLogger(Hl7Util.class);
static {
CHARACTER_REPLACEMENTS = new HashMap<>();
CHARACTER_REPLACEMENTS.put((char) 0x00, "<0x00 NUL>");
CHARACTER_REPLACEMENTS.put((char) 0x01, "<0x01 SOH>");
CHARACTER_REPLACEMENTS.put((char) 0x02, "<0x02 STX>");
CHARACTER_REPLACEMENTS.put((char) 0x03, "<0x03 ETX>");
CHARACTER_REPLACEMENTS.put((char) 0x04, "<0x04 EOT>");
CHARACTER_REPLACEMENTS.put((char) 0x05, "<0x05 ENQ>");
CHARACTER_REPLACEMENTS.put((char) 0x06, "<0x06 ACK>");
CHARACTER_REPLACEMENTS.put((char) 0x07, "<0x07 BEL>");
CHARACTER_REPLACEMENTS.put((char) 0x08, "<0x08 BS>");
CHARACTER_REPLACEMENTS.put((char) 0x09, "<0x09 TAB>");
CHARACTER_REPLACEMENTS.put((char) 0x0A, "<0x0A LF>");
CHARACTER_REPLACEMENTS.put((char) 0x0B, "<0x0B VT>");
CHARACTER_REPLACEMENTS.put((char) 0x0C, "<0x0C FF>");
CHARACTER_REPLACEMENTS.put((char) 0x0D, "<0x0D CR>");
CHARACTER_REPLACEMENTS.put((char) 0x0E, "<0x0E SO>");
CHARACTER_REPLACEMENTS.put((char) 0x0F, "<0x0F SI>");
CHARACTER_REPLACEMENTS.put((char) 0x10, "<0x10 DLE>");
CHARACTER_REPLACEMENTS.put((char) 0x11, "<0x11 DC1>");
CHARACTER_REPLACEMENTS.put((char) 0x12, "<0x12 DC2>");
CHARACTER_REPLACEMENTS.put((char) 0x13, "<0x13 DC3>");
CHARACTER_REPLACEMENTS.put((char) 0x14, "<0x14 DC4>");
CHARACTER_REPLACEMENTS.put((char) 0x15, "<0x15 NAK>");
CHARACTER_REPLACEMENTS.put((char) 0x16, "<0x16 SYN>");
CHARACTER_REPLACEMENTS.put((char) 0x17, "<0x17 ETB>");
CHARACTER_REPLACEMENTS.put((char) 0x18, "<0x18 CAN>");
CHARACTER_REPLACEMENTS.put((char) 0x19, "<0x19 EM>");
CHARACTER_REPLACEMENTS.put((char) 0x1A, "<0x1A SUB>");
CHARACTER_REPLACEMENTS.put((char) 0x1B, "<0x1B ESC>");
CHARACTER_REPLACEMENTS.put((char) 0x1C, "<0x1C FS>");
CHARACTER_REPLACEMENTS.put((char) 0x1D, "<0x1D GS>");
CHARACTER_REPLACEMENTS.put((char) 0x1E, "<0x1E RS>");
CHARACTER_REPLACEMENTS.put((char) 0x1F, "<0x1F US>");
CHARACTER_REPLACEMENTS.put((char) 0x7F, "<0x7F DEL>");
}
private final int logPhiMaxBytes;
private final boolean logPhi;
public Hl7Util(int logPhiMaxBytes, boolean logPhi) {
this.logPhiMaxBytes = logPhiMaxBytes;
this.logPhi = logPhi;
}
public int getLogPhiMaxBytes() {
return logPhiMaxBytes;
}
public String generateInvalidPayloadExceptionMessage(final byte[] hl7Bytes) {
if (hl7Bytes == null) {
return "HL7 payload is null";
}
return generateInvalidPayloadExceptionMessage(hl7Bytes, hl7Bytes.length);
}
/**
* Verifies that the HL7 payload array
*
* The MLLP protocol does not allow embedded START_OF_BLOCK or END_OF_BLOCK characters. The END_OF_DATA character is
* allowed (and expected) because it is also the segment delimiter for an HL7 message
*
* @param hl7Bytes the HL7 payload to validate
*
* @return If the payload is invalid, an error message suitable for inclusion in an exception is returned.
* If the payload is valid, null is returned;
*/
public static String generateInvalidPayloadExceptionMessage(final byte[] hl7Bytes, final int length) {
if (hl7Bytes == null) {
return "HL7 payload is null";
}
if (hl7Bytes.length <= 0) {
return "HL7 payload is empty";
}
if (length > hl7Bytes.length) {
LOG.warn(
"The length specified for the HL7 payload array <{}> is greater than the actual length of the array <{}> - only validating {} bytes",
length, hl7Bytes.length, hl7Bytes.length);
}
if (hl7Bytes.length < 3 || hl7Bytes[0] != 'M' || hl7Bytes[1] != 'S' || hl7Bytes[2] != 'H') {
return String.format("The first segment of the HL7 payload {%s} is not an MSH segment",
new String(hl7Bytes, 0, Math.min(3, hl7Bytes.length)));
}
int validationLength = Math.min(length, hl7Bytes.length);
if (hl7Bytes[validationLength - 1] != MllpProtocolConstants.SEGMENT_DELIMITER
&& hl7Bytes[validationLength - 1] != MllpProtocolConstants.MESSAGE_TERMINATOR) {
String format = "The HL7 payload terminating byte [%#x] is incorrect - expected [%#x] {ASCII []}";
return String.format(format, hl7Bytes[validationLength - 2], (byte) MllpProtocolConstants.SEGMENT_DELIMITER);
}
for (int i = 0; i < validationLength; ++i) {
switch (hl7Bytes[i]) {
case MllpProtocolConstants.START_OF_BLOCK:
return String.format("HL7 payload contains an embedded START_OF_BLOCK {%#x, ASCII } at index %d",
hl7Bytes[i], i);
case MllpProtocolConstants.END_OF_BLOCK:
return String.format("HL7 payload contains an embedded END_OF_BLOCK {%#x, ASCII } at index %d",
hl7Bytes[i], i);
default:
// continue on
}
}
return null;
}
/**
* Find the field separator indices in the Segment.
*
* NOTE: The last element of the list will be the index of the end of the segment.
*
* @param hl7MessageBytes the HL7 binary message
* @param startingIndex index of the beginning of the HL7 Segment
*
* @return List of the field separator indices, which may be empty.
*/
public static List findFieldSeparatorIndicesInSegment(byte[] hl7MessageBytes, int startingIndex) {
List fieldSeparatorIndices = new LinkedList<>();
if (hl7MessageBytes != null && hl7MessageBytes.length > startingIndex && hl7MessageBytes.length > 3) {
final byte fieldSeparator = hl7MessageBytes[3];
for (int i = startingIndex; i < hl7MessageBytes.length; ++i) {
if (fieldSeparator == hl7MessageBytes[i]) {
fieldSeparatorIndices.add(i);
} else if (MllpProtocolConstants.SEGMENT_DELIMITER == hl7MessageBytes[i]) {
fieldSeparatorIndices.add(i);
break;
}
}
}
return fieldSeparatorIndices;
}
/**
* Find the String value of MSH-18 (Character set).
*
* @param hl7Message the HL7 binary data to search
*
* @return the String value of MSH-18, or an empty String if not found.
*/
public String findMsh18(byte[] hl7Message, Charset charset) {
String answer = "";
if (hl7Message != null && hl7Message.length > 0) {
List fieldSeparatorIndexes = findFieldSeparatorIndicesInSegment(hl7Message, 0);
if (fieldSeparatorIndexes.size() > 17) {
int startOfMsh19 = fieldSeparatorIndexes.get(16) + 1;
int length = fieldSeparatorIndexes.get(17) - fieldSeparatorIndexes.get(16) - 1;
if (length > 0) {
answer = new String(hl7Message, startOfMsh19, length, charset);
}
}
}
return answer;
}
public void generateAcknowledgementPayload(
MllpSocketBuffer mllpSocketBuffer, byte[] hl7MessageBytes, String acknowledgementCode)
throws MllpAcknowledgementGenerationException {
generateAcknowledgementPayload(mllpSocketBuffer, hl7MessageBytes, acknowledgementCode, null);
}
public void generateAcknowledgementPayload(
MllpSocketBuffer mllpSocketBuffer, byte[] hl7MessageBytes, String acknowledgementCode, String msa3)
throws MllpAcknowledgementGenerationException {
if (hl7MessageBytes == null) {
throw new MllpAcknowledgementGenerationException("Null HL7 message received for parsing operation", logPhi);
}
List fieldSeparatorIndexes = findFieldSeparatorIndicesInSegment(hl7MessageBytes, 0);
if (fieldSeparatorIndexes.isEmpty()) {
throw new MllpAcknowledgementGenerationException(
"Failed to find the end of the MSH Segment while attempting to generate response", hl7MessageBytes, logPhi);
}
if (fieldSeparatorIndexes.size() < 8) {
String exceptionMessage = String.format(
"Insufficient number of fields found in MSH to generate a response - 10 are required but %d were found",
fieldSeparatorIndexes.size() - 1);
throw new MllpAcknowledgementGenerationException(exceptionMessage, hl7MessageBytes, logPhi);
}
final byte fieldSeparator = hl7MessageBytes[3];
// Start building the MLLP Envelope
mllpSocketBuffer.openMllpEnvelope();
// Build the MSH Segment
mllpSocketBuffer.write(hl7MessageBytes, 0, fieldSeparatorIndexes.get(1)); // through MSH-2 (without trailing field separator)
writeFieldToBuffer(3, mllpSocketBuffer, hl7MessageBytes, fieldSeparatorIndexes); // MSH-5
writeFieldToBuffer(4, mllpSocketBuffer, hl7MessageBytes, fieldSeparatorIndexes); // MSH-6
writeFieldToBuffer(1, mllpSocketBuffer, hl7MessageBytes, fieldSeparatorIndexes); // MSH-3
writeFieldToBuffer(2, mllpSocketBuffer, hl7MessageBytes, fieldSeparatorIndexes); // MSH-4
// MSH-7
mllpSocketBuffer.write(fieldSeparator);
// TODO static field TIMESTAMP_FORMAT of type java.text.DateFormat isn't thread safe! Think about using ThreadLocal
mllpSocketBuffer.write(TIMESTAMP_FORMAT.format(new Date()).getBytes());
// Don't copy MSH-8
mllpSocketBuffer.write(fieldSeparator);
// Need to generate the correct MSH-9
mllpSocketBuffer.write(fieldSeparator);
mllpSocketBuffer.write("ACK".getBytes()); // MSH-9.1
int msh92start = -1;
for (int j = fieldSeparatorIndexes.get(7) + 1; j < fieldSeparatorIndexes.get(8); ++j) {
final byte componentSeparator = hl7MessageBytes[4];
if (componentSeparator == hl7MessageBytes[j]) {
msh92start = j;
break;
}
}
// MSH-9.2
if (-1 == msh92start) {
LOG.warn("Didn't find component separator for MSH-9.2 - sending ACK in MSH-9");
} else {
mllpSocketBuffer.write(hl7MessageBytes, msh92start, fieldSeparatorIndexes.get(8) - msh92start);
}
// MSH-10 - use the original control ID, but add an "A" as a suffix
writeFieldToBuffer(8, mllpSocketBuffer, hl7MessageBytes, fieldSeparatorIndexes);
if (fieldSeparatorIndexes.get(9) - fieldSeparatorIndexes.get(8) > 1) {
mllpSocketBuffer.write('A');
}
// MSH-10 through the end of the MSH
mllpSocketBuffer.write(hl7MessageBytes, fieldSeparatorIndexes.get(9),
fieldSeparatorIndexes.get(fieldSeparatorIndexes.size() - 1) - fieldSeparatorIndexes.get(9));
mllpSocketBuffer.write(MllpProtocolConstants.SEGMENT_DELIMITER);
// Build the MSA Segment
mllpSocketBuffer.write("MSA".getBytes(), 0, 3);
mllpSocketBuffer.write(fieldSeparator);
mllpSocketBuffer.write(acknowledgementCode.getBytes(), 0, 2);
writeFieldToBuffer(8, mllpSocketBuffer, hl7MessageBytes, fieldSeparatorIndexes); // MSH-10 => MSA-2
if (msa3 != null && !msa3.isEmpty()) {
mllpSocketBuffer.write(fieldSeparator);
mllpSocketBuffer.write(msa3.getBytes());
}
mllpSocketBuffer.write(MllpProtocolConstants.SEGMENT_DELIMITER);
// Close the MLLP Envelope
mllpSocketBuffer.write(MllpProtocolConstants.PAYLOAD_TERMINATOR);
return;
}
public String convertToPrintFriendlyString(String phiString) {
if (null == phiString) {
return NULL_REPLACEMENT_VALUE;
} else if (phiString.isEmpty()) {
return EMPTY_REPLACEMENT_VALUE;
}
int conversionLength = (logPhiMaxBytes > 0) ? Integer.min(phiString.length(), logPhiMaxBytes) : phiString.length();
StringBuilder builder = new StringBuilder(conversionLength + STRING_BUFFER_PAD_SIZE);
for (int i = 0; i < conversionLength; ++i) {
appendCharacterAsPrintFriendlyString(builder, phiString.charAt(i));
}
return builder.toString();
}
public String convertToPrintFriendlyString(byte[] phiBytes) {
return bytesToPrintFriendlyStringBuilder(phiBytes).toString();
}
/**
* Convert a PHI byte[] to a String, replacing specific non-printable characters with readable strings.
*
* NOTE: this conversion uses the default character set, so not all characters my convert correctly.
*
* @param phiBytes the PHI byte[] to log
* @param startPosition the starting position/index of the data
* @param endPosition the ending position/index of the data - will not be included in String
*
* @return a String representation of the byte[]
*/
public String convertToPrintFriendlyString(byte[] phiBytes, int startPosition, int endPosition) {
return bytesToPrintFriendlyStringBuilder(phiBytes, startPosition, endPosition).toString();
}
/**
* Convert a PHI byte[] to a StringBuilder, replacing specific non-printable characters with readable strings.
*
* NOTE: this conversion uses the default character set, so not all characters my convert correctly.
*
* @param phiBytes the PHI byte[] to log
*
* @return
*/
public StringBuilder bytesToPrintFriendlyStringBuilder(byte[] phiBytes) {
return bytesToPrintFriendlyStringBuilder(phiBytes, 0, phiBytes != null ? phiBytes.length : -1);
}
/**
* Convert a PHI byte[] to a StringBuilder, replacing specific non-printable characters with readable strings.
*
* NOTE: this conversion uses the default character set, so not all characters my convert correctly.
*
* @param phiBytes the PHI byte[] to log
* @param startPosition the starting position/index of the data
* @param endPosition the ending position/index of the data - will not be included in StringBuilder
*
* @return a String representation of the byte[]
*/
public StringBuilder bytesToPrintFriendlyStringBuilder(
byte[] phiBytes, int startPosition, int endPosition) {
StringBuilder answer = new StringBuilder();
appendBytesAsPrintFriendlyString(answer, phiBytes, startPosition, endPosition);
return answer;
}
public void appendBytesAsPrintFriendlyString(StringBuilder builder, byte[] phiBytes) {
appendBytesAsPrintFriendlyString(builder, phiBytes, 0, phiBytes != null ? phiBytes.length : 0);
}
/**
* Append a PHI byte[] to a StringBuilder, replacing specific non-printable characters with readable strings.
*
* NOTE: this conversion uses the default character set, so not all characters my convert correctly.
*
* @param phiBytes the PHI byte[] to log
* @param startPosition the starting position/index of the data
* @param endPosition the ending position/index of the data - will not be included in String
*/
public void appendBytesAsPrintFriendlyString(
StringBuilder builder, byte[] phiBytes, int startPosition, int endPosition) {
if (builder == null) {
throw new IllegalArgumentException("StringBuilder cannot be null");
}
if (null == phiBytes) {
builder.append(NULL_REPLACEMENT_VALUE);
} else if (phiBytes.length == 0) {
builder.append(EMPTY_REPLACEMENT_VALUE);
} else if (startPosition <= endPosition) {
if (startPosition < 0) {
startPosition = 0;
}
if (startPosition < phiBytes.length) {
if (endPosition >= -1) {
if (endPosition == -1 || endPosition >= phiBytes.length) {
endPosition = phiBytes.length;
}
int length = endPosition - startPosition;
if (length > 0) {
int conversionLength = (logPhiMaxBytes > 0) ? Integer.min(length, logPhiMaxBytes) : length;
if (builder.capacity() - builder.length() < conversionLength + STRING_BUFFER_PAD_SIZE) {
builder.ensureCapacity(builder.length() + conversionLength + STRING_BUFFER_PAD_SIZE);
}
for (int i = 0; i < conversionLength; ++i) {
appendCharacterAsPrintFriendlyString(builder, (char) phiBytes[startPosition + i]);
}
}
}
}
}
}
static void appendCharacterAsPrintFriendlyString(StringBuilder builder, char c) {
if (CHARACTER_REPLACEMENTS.containsKey(c)) {
builder.append(CHARACTER_REPLACEMENTS.get(c));
} else {
builder.append(c);
}
}
public static String getCharacterAsPrintFriendlyString(char c) {
if (CHARACTER_REPLACEMENTS.containsKey(c)) {
return CHARACTER_REPLACEMENTS.get(c);
}
return String.valueOf(c);
}
/**
* Copy a field from the HL7 Message Bytes to the supplied MllpSocketBuffer.
*
* NOTE: Internal function - no error checking
*
* @param mllpSocketBuffer the destination for the field
* @param hl7MessageBytes the HL7 message bytes
* @param fieldSeparatorIndexes the list of the indices of the field separators
*/
private static void writeFieldToBuffer(
int fieldNumber, MllpSocketBuffer mllpSocketBuffer, byte[] hl7MessageBytes, List fieldSeparatorIndexes) {
mllpSocketBuffer.write(hl7MessageBytes, fieldSeparatorIndexes.get(fieldNumber),
fieldSeparatorIndexes.get(fieldNumber + 1) - fieldSeparatorIndexes.get(fieldNumber));
}
}