/** * 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.hl7; import java.nio.charset.CharacterCodingException; import java.nio.charset.CharsetDecoder; import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.CumulativeProtocolDecoder; import org.apache.mina.filter.codec.ProtocolDecoderOutput; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * HL7MLLPDecoder that is aware that a HL7 message can span several buffers. * In addition, it avoids rescanning packets by keeping state in the IOSession. */ class HL7MLLPDecoder extends CumulativeProtocolDecoder { private static final Logger LOG = LoggerFactory.getLogger(HL7MLLPDecoder.class); private static final String DECODER_STATE = HL7MLLPDecoder.class.getName() + ".STATE"; private static final String CHARSET_DECODER = HL7MLLPDecoder.class.getName() + ".charsetdecoder"; private HL7MLLPConfig config; HL7MLLPDecoder(HL7MLLPConfig config) { this.config = config; } @Override protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception { // Get the state of the current message and // Skip what we have already scanned before DecoderState state = decoderState(session); in.position(state.current()); LOG.debug("Received data, checking from position {} to {}", in.position(), in.limit()); boolean messageDecoded = false; while (in.hasRemaining()) { int previousPosition = in.position(); byte current = in.get(); // Check if we are at the end of an HL7 message if (current == config.getEndByte2() && state.previous() == config.getEndByte1()) { if (state.isStarted()) { // Save the current buffer pointers and reset them to surround the identifier message int currentPosition = in.position(); int currentLimit = in.limit(); LOG.debug("Message ends at position {} with length {}", previousPosition, previousPosition - state.start() + 1); in.position(state.start()); in.limit(currentPosition); LOG.debug("Set start to position {} and limit to {}", in.position(), in.limit()); // Now create string or byte[] from this part of the buffer and restore the buffer pointers try { out.write(config.isProduceString() ? parseMessageToString(in.slice(), charsetDecoder(session)) : parseMessageToByteArray(in.slice())); messageDecoded = true; } finally { LOG.debug("Resetting to position {} and limit to {}", currentPosition, currentLimit); in.position(currentPosition); in.limit(currentLimit); state.reset(); } } else { LOG.warn("Ignoring message end at position {} until start byte has been seen.", previousPosition); } } else { // Check if we are at the start of an HL7 message if (current == config.getStartByte()) { state.markStart(previousPosition); } else { // Remember previous byte in state object because the buffer could // be theoretically exhausted right between the two end bytes state.markPrevious(current); } messageDecoded = false; } } if (!messageDecoded) { // Could not find a complete message in the buffer. // Reset to the initial position (just as nothing had been read yet) // and return false so that this method is called again with more data. LOG.debug("No complete message yet at position {} ", in.position()); state.markCurrent(in.position()); in.position(0); } return messageDecoded; } // Make a defensive byte copy (the buffer will be reused) // and omit the start and the two end bytes of the MLLP message // returning a byte array private Object parseMessageToByteArray(IoBuffer buf) throws CharacterCodingException { int len = buf.limit() - 3; LOG.debug("Making byte array of length {}", len); byte[] dst = new byte[len]; buf.skip(1); // skip start byte buf.get(dst, 0, len); buf.skip(2); // skip end bytes // Only do this if conversion is enabled if (config.isConvertLFtoCR()) { LOG.debug("Replacing LF by CR"); for (int i = 0; i < dst.length; i++) { if (dst[i] == (byte) '\n') { dst[i] = (byte) '\r'; } } } return dst; } // Make a defensive byte copy (the buffer will be reused) // and omit the start and the two end bytes of the MLLP message // returning a String private Object parseMessageToString(IoBuffer buf, CharsetDecoder decoder) throws CharacterCodingException { int len = buf.limit() - 3; LOG.debug("Making string of length {} using charset {}", len, decoder.charset()); buf.skip(1); // skip start byte String message = buf.getString(len, decoder); buf.skip(2); // skip end bytes // Only do this if conversion is enabled if (config.isConvertLFtoCR()) { LOG.debug("Replacing LF by CR"); message = message.replace('\n', '\r'); } return message; } @Override public void dispose(IoSession session) throws Exception { session.removeAttribute(DECODER_STATE); session.removeAttribute(CHARSET_DECODER); } private CharsetDecoder charsetDecoder(IoSession session) { synchronized (session) { CharsetDecoder decoder = (CharsetDecoder) session.getAttribute(CHARSET_DECODER); if (decoder == null) { decoder = config.getCharset().newDecoder() .onMalformedInput(config.getMalformedInputErrorAction()) .onUnmappableCharacter(config.getUnmappableCharacterErrorAction()); session.setAttribute(CHARSET_DECODER, decoder); } return decoder; } } private DecoderState decoderState(IoSession session) { synchronized (session) { DecoderState decoderState = (DecoderState) session.getAttribute(DECODER_STATE); if (decoderState == null) { decoderState = new DecoderState(); session.setAttribute(DECODER_STATE, decoderState); } return decoderState; } } /** * Holds the state of the decoding process */ private static class DecoderState { private int startPos = -1; private int currentPos; private byte previousByte; void reset() { startPos = -1; currentPos = 0; previousByte = 0; } void markStart(int position) { if (isStarted()) { LOG.warn("Ignoring message start at position {} before previous message has ended.", position); } else { startPos = position; LOG.debug("Message starts at position {}", startPos); } } void markCurrent(int position) { currentPos = position; } void markPrevious(byte previous) { previousByte = previous; } public int start() { return startPos; } public int current() { return currentPos; } public byte previous() { return previousByte; } public boolean isStarted() { return startPos >= 0; } } }