This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 394deda18cf8 CAMEL-26433: camel-hl7 - add a maxFrameLength option to
the HL7 MLLP MINA codec (#29043)
394deda18cf8 is described below
commit 394deda18cf817cafe5472cb2dd99428088df612
Author: allthingssecurity <[email protected]>
AuthorDate: Tue Nov 3 16:09:17 2026 +0530
CAMEL-26433: camel-hl7 - add a maxFrameLength option to the HL7 MLLP MINA
codec (#29043)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../apache/camel/catalog/docs/hl7-dataformat.adoc | 31 ++-
.../camel-hl7/src/main/docs/hl7-dataformat.adoc | 31 ++-
.../apache/camel/component/hl7/HL7MLLPCodec.java | 14 +
.../apache/camel/component/hl7/HL7MLLPConfig.java | 7 +-
.../apache/camel/component/hl7/HL7MLLPDecoder.java | 141 +++++++++-
.../hl7/HL7MLLPDecoderMaxFrameLengthTest.java | 291 +++++++++++++++++++++
.../hl7/HL7MLLPMinaMaxFrameLengthRouteTest.java | 89 +++++++
7 files changed, 580 insertions(+), 24 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hl7-dataformat.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hl7-dataformat.adoc
index bdbf5edf75b8..5640be46334a 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hl7-dataformat.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hl7-dataformat.adoc
@@ -70,14 +70,20 @@ the HL7 message content.
|`convertLFtoCR` |`false` |Will convert `\n` to `\r` (`0x0d`, 13 decimal) as
HL7 stipulates `\r` as
segment terminators. The HAPI library requires the use of `\r`.
-|`maxFrameLength` |`2147483647` |Netty decoder (`HL7MLLPNettyDecoderFactory`)
only: the maximum length in bytes of
-a message, counted from the start of the data up to the end bytes (the start
byte included, the end bytes not). A
-longer message is discarded and fails with a `TooLongFrameException`: the
Netty consumer reports it to its exception
-handler and closes the connection. The decoder fails as soon as more bytes
than the limit have arrived without both end
-bytes, so a message of exactly this length fails when its first end byte
arrives without the second; set the limit
-with some margin over the largest message you expect. The default
(`Integer.MAX_VALUE`) means no limit.
+|`maxFrameLength` |`2147483647` |The maximum length in bytes of a message, for
the MINA codec (`HL7MLLPCodec`) and the
+Netty decoder (`HL7MLLPNettyDecoderFactory`). The default
(`Integer.MAX_VALUE`) means no limit.
|===
+The `maxFrameLength` limit counts from the start of the data up to the end
bytes (the start byte included, the end
+bytes not). Both codecs fail as soon as a message is longer than the limit,
without waiting for its end bytes, and
+discard the rest of the long message up to its end bytes:
+
+* MINA codec: the message fails with a `ProtocolDecoderException`, and the
rest of the data received with it is
+discarded too. The MINA consumer closes the connection, and a MINA producer
fails the exchange when the reply is longer.
+* Netty decoder: the message fails with a `TooLongFrameException`, which the
Netty consumer reports to its exception
+handler before it closes the connection. The Netty decoder counts a first end
byte that arrives without the second, so a
+message of exactly the limit can fail: set the limit with some margin over the
largest message you expect.
+
=== Exposing an HL7 listener using Mina
In the Spring XML file, we configure a mina endpoint to listen for HL7
@@ -103,6 +109,19 @@ up in the Spring XML file:
</bean>
----
+By default the codec accepts messages of any size. To limit the size of a
message, set `maxFrameLength` (in bytes)
+on the codec; a longer message fails with a `ProtocolDecoderException` and the
connection is closed (see
+`maxFrameLength` in the options above):
+
+._XML-only: Spring bean declaration for HL7 MLLP codec with a message size
limit_
+[source,xml]
+----
+<bean id="hl7codec" class="org.apache.camel.component.hl7.HL7MLLPCodec">
+ <property name="charset" value="iso-8859-1"/>
+ <property name="maxFrameLength" value="1048576"/>
+</bean>
+----
+
The endpoint *hl7MinaLlistener* can then be used in a route as a
consumer, as this Java DSL example illustrates:
diff --git a/components/camel-hl7/src/main/docs/hl7-dataformat.adoc
b/components/camel-hl7/src/main/docs/hl7-dataformat.adoc
index bdbf5edf75b8..5640be46334a 100644
--- a/components/camel-hl7/src/main/docs/hl7-dataformat.adoc
+++ b/components/camel-hl7/src/main/docs/hl7-dataformat.adoc
@@ -70,14 +70,20 @@ the HL7 message content.
|`convertLFtoCR` |`false` |Will convert `\n` to `\r` (`0x0d`, 13 decimal) as
HL7 stipulates `\r` as
segment terminators. The HAPI library requires the use of `\r`.
-|`maxFrameLength` |`2147483647` |Netty decoder (`HL7MLLPNettyDecoderFactory`)
only: the maximum length in bytes of
-a message, counted from the start of the data up to the end bytes (the start
byte included, the end bytes not). A
-longer message is discarded and fails with a `TooLongFrameException`: the
Netty consumer reports it to its exception
-handler and closes the connection. The decoder fails as soon as more bytes
than the limit have arrived without both end
-bytes, so a message of exactly this length fails when its first end byte
arrives without the second; set the limit
-with some margin over the largest message you expect. The default
(`Integer.MAX_VALUE`) means no limit.
+|`maxFrameLength` |`2147483647` |The maximum length in bytes of a message, for
the MINA codec (`HL7MLLPCodec`) and the
+Netty decoder (`HL7MLLPNettyDecoderFactory`). The default
(`Integer.MAX_VALUE`) means no limit.
|===
+The `maxFrameLength` limit counts from the start of the data up to the end
bytes (the start byte included, the end
+bytes not). Both codecs fail as soon as a message is longer than the limit,
without waiting for its end bytes, and
+discard the rest of the long message up to its end bytes:
+
+* MINA codec: the message fails with a `ProtocolDecoderException`, and the
rest of the data received with it is
+discarded too. The MINA consumer closes the connection, and a MINA producer
fails the exchange when the reply is longer.
+* Netty decoder: the message fails with a `TooLongFrameException`, which the
Netty consumer reports to its exception
+handler before it closes the connection. The Netty decoder counts a first end
byte that arrives without the second, so a
+message of exactly the limit can fail: set the limit with some margin over the
largest message you expect.
+
=== Exposing an HL7 listener using Mina
In the Spring XML file, we configure a mina endpoint to listen for HL7
@@ -103,6 +109,19 @@ up in the Spring XML file:
</bean>
----
+By default the codec accepts messages of any size. To limit the size of a
message, set `maxFrameLength` (in bytes)
+on the codec; a longer message fails with a `ProtocolDecoderException` and the
connection is closed (see
+`maxFrameLength` in the options above):
+
+._XML-only: Spring bean declaration for HL7 MLLP codec with a message size
limit_
+[source,xml]
+----
+<bean id="hl7codec" class="org.apache.camel.component.hl7.HL7MLLPCodec">
+ <property name="charset" value="iso-8859-1"/>
+ <property name="maxFrameLength" value="1048576"/>
+</bean>
+----
+
The endpoint *hl7MinaLlistener* can then be used in a route as a
consumer, as this Java DSL example illustrates:
diff --git
a/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPCodec.java
b/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPCodec.java
index 237d07b392e6..2d67636dbd54 100644
---
a/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPCodec.java
+++
b/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPCodec.java
@@ -118,4 +118,18 @@ public class HL7MLLPCodec implements ProtocolCodecFactory {
public void setProduceString(boolean apply) {
config.setProduceString(apply);
}
+
+ public int getMaxFrameLength() {
+ return config.getMaxFrameLength();
+ }
+
+ /**
+ * The maximum length in bytes of a message that the decoder reads,
counted from the start of the data up to the end
+ * bytes (the start byte included, the end bytes not). A longer message is
discarded, with the rest of the data
+ * received with it, and fails with a {@code ProtocolDecoderException}; a
MINA consumer closes the connection, and a
+ * MINA producer fails the exchange when a reply is longer. The default is
{@link Integer#MAX_VALUE} (no limit).
+ */
+ public void setMaxFrameLength(int maxFrameLength) {
+ config.setMaxFrameLength(maxFrameLength);
+ }
}
diff --git
a/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPConfig.java
b/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPConfig.java
index 8e7a0aed356b..79159a2940fa 100644
---
a/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPConfig.java
+++
b/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPConfig.java
@@ -142,9 +142,10 @@ public class HL7MLLPConfig {
}
/**
- * The maximum length in bytes of a message that the Netty decoder reads,
counted from the start of the data up to
- * the end bytes (the start byte included, the end bytes not). A longer
message is discarded and fails with a
- * {@code TooLongFrameException}. The default is {@link Integer#MAX_VALUE}
(no limit).
+ * The maximum length in bytes of a message that the decoder (Netty or
MINA) reads, counted from the start of the
+ * data up to the end bytes (the start byte included, the end bytes not).
A longer message is discarded and fails
+ * with a {@code TooLongFrameException} (Netty) or a {@code
ProtocolDecoderException} (MINA). The default is
+ * {@link Integer#MAX_VALUE} (no limit).
*/
public void setMaxFrameLength(int maxFrameLength) {
if (maxFrameLength <= 0) {
diff --git
a/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPDecoder.java
b/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPDecoder.java
index 842ce5030a38..a3dfc7844b5b 100644
---
a/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPDecoder.java
+++
b/components/camel-hl7/src/main/java/org/apache/camel/component/hl7/HL7MLLPDecoder.java
@@ -22,6 +22,7 @@ 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.ProtocolDecoderException;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -40,6 +41,10 @@ class HL7MLLPDecoder extends CumulativeProtocolDecoder {
HL7MLLPDecoder(HL7MLLPConfig config) {
this.config = config;
+ // keep the data of an incomplete message between two reads also on a
transport that MINA does not regard as
+ // fragmenting (mina:udp, mina:vm): without it MINA hands each read to
doDecode on its own, so the positions
+ // that this decoder keeps would point into a read that MINA did not
keep
+ setTransportMetadataFragmentation(false);
}
@Override
@@ -50,13 +55,22 @@ class HL7MLLPDecoder extends CumulativeProtocolDecoder {
DecoderState state = decoderState(session);
in.position(state.current());
+ if (state.isDiscarding() && !skipDiscarded(state, in)) {
+ // the rest of a message that was too long: drop what was
received, and wait for its end bytes
+ LOG.debug("Discarded {} bytes of a message longer than
maxFrameLength", in.limit() - state.current());
+ in.position(in.limit());
+ state.markCurrent(0);
+ return false;
+ }
+
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();
+ // unsigned, to compare it with the configured start and end bytes
(chars), also above 0x7F
+ int current = in.get() & 0xFF;
// Check if we are at the end of an HL7 message
if (current == config.getEndByte2() && state.previous() ==
config.getEndByte1()) {
@@ -81,11 +95,22 @@ class HL7MLLPDecoder extends CumulativeProtocolDecoder {
in.position(currentPosition);
in.limit(currentLimit);
state.reset();
+ // the data of the next message starts after these end
bytes
+ state.markDataStart(currentPosition);
}
} else {
LOG.warn("Ignoring message end at position {} until start
byte has been seen.", previousPosition);
+ // the data of the next message starts after these end
bytes
+ state.markDataStart(in.position());
}
} else {
+ // the bytes of the message so far, from the start of its
data; an end byte 1 is not counted yet, as the
+ // end byte 2 may follow it
+ int length = in.position() - state.dataStart() - (current ==
config.getEndByte1() ? 1 : 0);
+ if (length > config.getMaxFrameLength()) {
+ state.markPrevious(current);
+ tooLong(session, in, state);
+ }
// Check if we are at the start of an HL7 message
if (current == config.getStartByte()) {
state.markStart(previousPosition);
@@ -99,16 +124,81 @@ class HL7MLLPDecoder extends CumulativeProtocolDecoder {
}
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.
+ // Could not find a complete message in the buffer: return false
so that this method is called again
+ // with more data. Keep only the data of the current message: MINA
keeps the bytes from this position for
+ // the next call (so the bytes of the messages decoded before, and
of end bytes without a start, do not
+ // pile up), and the positions of the state are moved down by as
much
LOG.debug("No complete message yet at position {}", in.position());
- state.markCurrent(in.position());
- in.position(0);
+ int dataStart = state.dataStart();
+ state.markCurrent(in.position() - dataStart);
+ state.rebase(dataStart);
+ in.position(dataStart);
+ } else {
+ // the buffer is used up to the end of this message, so the next
data starts at the start of the buffer
+ state.markDataStart(0);
}
return messageDecoded;
}
+ /**
+ * Skips the bytes of a message that was too long up to its end bytes.
+ *
+ * @return true when its end bytes were found, and the data of the next
message starts after them
+ */
+ private boolean skipDiscarded(DecoderState state, IoBuffer in) {
+ while (in.hasRemaining()) {
+ int current = in.get() & 0xFF;
+ if (current == config.getEndByte2() && state.previous() ==
config.getEndByte1()) {
+ LOG.debug("End of the discarded message at position {}",
in.position() - 1);
+ state.stopDiscarding();
+ state.markPrevious(0);
+ state.markDataStart(in.position());
+ return true;
+ }
+ state.markPrevious(current);
+ }
+ return false;
+ }
+
+ /**
+ * Discards a message longer than maxFrameLength, with the rest of the
data received with it, and fails.
+ */
+ private void tooLong(IoSession session, IoBuffer in, DecoderState state)
throws Exception {
+ // the end bytes of this message may already be in the rest of the
data: then the next message starts after them
+ boolean discarding = !skipDiscarded(state, in);
+ int previous = state.previous();
+ in.position(in.limit());
+ state.reset();
+ state.markDataStart(0);
+ state.markPrevious(previous);
+ if (discarding) {
+ state.startDiscarding();
+ }
+ // the data is used up: drop the buffer that MINA keeps for this
session, so that it is not read again (this
+ // decoder throws, so MINA does not do it)
+ super.dispose(session);
+ throw new FrameTooLongException(
+ "HL7 MLLP message is longer than maxFrameLength of " +
config.getMaxFrameLength() + " bytes: discarded");
+ }
+
+ /**
+ * The failure of a message longer than maxFrameLength. It has an empty
hex dump, so that MINA does not add the data
+ * that was received (the message) to it, and so to the logs
(ProtocolCodecFilter dumps the data only when the
+ * exception has none).
+ */
+ static final class FrameTooLongException extends ProtocolDecoderException {
+ private static final long serialVersionUID = 1L;
+
+ FrameTooLongException(String message) {
+ super(message);
+ }
+
+ @Override
+ public String getHexdump() {
+ return "";
+ }
+ }
+
// 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
@@ -186,7 +276,12 @@ class HL7MLLPDecoder extends CumulativeProtocolDecoder {
private static class DecoderState {
private int startPos = -1;
private int currentPos;
- private byte previousByte;
+ // unsigned (0 to 0xFF)
+ private int previousByte;
+ // where the data of the current message starts in the buffer: after
the end bytes of the previous message
+ private int dataStartPos;
+ // whether the rest of a message that was too long is being skipped
+ private boolean discarding;
void reset() {
startPos = -1;
@@ -207,7 +302,35 @@ class HL7MLLPDecoder extends CumulativeProtocolDecoder {
currentPos = position;
}
- void markPrevious(byte previous) {
+ void markDataStart(int position) {
+ dataStartPos = position;
+ }
+
+ /** Moves the positions down by {@code offset}, as the first {@code
offset} bytes of the buffer are dropped. */
+ void rebase(int offset) {
+ if (startPos >= 0) {
+ startPos -= offset;
+ }
+ dataStartPos -= offset;
+ }
+
+ int dataStart() {
+ return dataStartPos;
+ }
+
+ void startDiscarding() {
+ discarding = true;
+ }
+
+ void stopDiscarding() {
+ discarding = false;
+ }
+
+ boolean isDiscarding() {
+ return discarding;
+ }
+
+ void markPrevious(int previous) {
previousByte = previous;
}
@@ -219,7 +342,7 @@ class HL7MLLPDecoder extends CumulativeProtocolDecoder {
return currentPos;
}
- public byte previous() {
+ public int previous() {
return previousByte;
}
diff --git
a/components/camel-hl7/src/test/java/org/apache/camel/component/hl7/HL7MLLPDecoderMaxFrameLengthTest.java
b/components/camel-hl7/src/test/java/org/apache/camel/component/hl7/HL7MLLPDecoderMaxFrameLengthTest.java
new file mode 100644
index 000000000000..515b4fa38098
--- /dev/null
+++
b/components/camel-hl7/src/test/java/org/apache/camel/component/hl7/HL7MLLPDecoderMaxFrameLengthTest.java
@@ -0,0 +1,291 @@
+/*
+ * 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.lang.reflect.Proxy;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.mina.core.buffer.IoBuffer;
+import org.apache.mina.core.filterchain.IoFilter;
+import org.apache.mina.filter.codec.ProtocolCodecFilter;
+import org.apache.mina.filter.codec.ProtocolCodecSession;
+import org.apache.mina.filter.codec.ProtocolDecoder;
+import org.apache.mina.filter.codec.ProtocolDecoderException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests the maxFrameLength option of the HL7 MLLP MINA decoder. The decoder
keeps the data that has no complete message
+ * yet between two calls, on a TCP session and also on a session without
fragmentation, such as the test session.
+ */
+class HL7MLLPDecoderMaxFrameLengthTest {
+
+ private static final String START = "\u000b";
+ private static final String END = "\u001c\r";
+
+ private final ProtocolCodecSession session = new ProtocolCodecSession();
+
+ @Test
+ void messageAtTheLimitIsDecoded() throws Exception {
+ // the start byte and 49 bytes of message: 50 bytes before the end
bytes
+ String message = "A".repeat(49);
+ ProtocolDecoder decoder = decoder(50);
+
+ decode(decoder, START + message + END);
+
+ assertEquals(message, session.getDecoderOutputQueue().poll());
+ assertNull(session.getDecoderOutputQueue().poll());
+ }
+
+ @Test
+ void messageAtTheLimitIsDecodedWhenItsEndBytesArriveApart() throws
Exception {
+ // unlike the Netty decoder, an end byte 1 that arrives without the
end byte 2 is not counted yet
+ String message = "A".repeat(49);
+ ProtocolDecoder decoder = decoder(50);
+
+ decode(decoder, START + message + "\u001c");
+ decode(decoder, "\r");
+
+ assertEquals(message, session.getDecoderOutputQueue().poll());
+ }
+
+ @Test
+ void messageOverTheLimitFails() throws Exception {
+ ProtocolDecoder decoder = decoder(50);
+
+ ProtocolDecoderException e =
assertThrows(ProtocolDecoderException.class,
+ () -> decode(decoder, START + "A".repeat(50) + END));
+
+ assertTrue(e.getMessage().startsWith("HL7 MLLP message is longer than
maxFrameLength of 50 bytes"),
+ e.getMessage());
+ // MINA adds a hex dump of the received data (the message) to a
decoder exception that has none, and the
+ // consumer logs the exception: this one has an empty dump
+ assertEquals("", e.getHexdump());
+ assertFalse(e.getMessage().contains("Hexdump"), e.getMessage());
+ assertNull(session.getDecoderOutputQueue().poll());
+
+ // its end bytes came with it: the next message is decoded
+ decode(decoder, START + "next" + END);
+ assertEquals("next", session.getDecoderOutputQueue().poll());
+ }
+
+ @Test
+ void messageOverTheLimitOverTwoReadsFailsAndTheNextMessageIsDecoded()
throws Exception {
+ ProtocolDecoder decoder = decoder(50);
+
+ // the first part is kept for the next read; the second part goes over
the limit and has the end bytes
+ decode(decoder, START + "B".repeat(30));
+ assertThrows(ProtocolDecoderException.class, () -> decode(decoder,
"B".repeat(30) + END));
+ assertNull(session.getDecoderOutputQueue().poll());
+
+ // nothing of the long message is read again: the next message is
decoded
+ decode(decoder, START + "next" + END);
+ assertEquals("next", session.getDecoderOutputQueue().poll());
+ assertNull(session.getDecoderOutputQueue().poll());
+ }
+
+ @Test
+ void dataWithoutEndBytesFailsOnceOverTheLimitAndTheNextMessageIsDecoded()
throws Exception {
+ ProtocolDecoder decoder = decoder(50);
+
+ // a peer that sends more than the limit without the end bytes: the
decoder does not wait for them, in one read
+ // or over several reads
+ decode(decoder, START + "B".repeat(30));
+ assertThrows(ProtocolDecoderException.class, () -> decode(decoder,
"B".repeat(30)));
+ assertNull(session.getDecoderOutputQueue().poll());
+
+ // the rest of the long message is discarded up to its end bytes (over
several reads too), and the next message
+ // is decoded
+ decode(decoder, "B".repeat(100));
+ decode(decoder, "B".repeat(20) + "\u001c");
+ decode(decoder, "\r" + START + "next" + END);
+ assertEquals("next", session.getDecoderOutputQueue().poll());
+ assertNull(session.getDecoderOutputQueue().poll());
+ }
+
+ @Test
+ void dataBeforeTheStartByteCounts() throws Exception {
+ // the limit counts from the start of the data, as the Netty decoder
does: bytes before the start byte count too
+ ProtocolDecoder decoder = decoder(50);
+
+ assertThrows(ProtocolDecoderException.class, () -> decode(decoder,
"x".repeat(45) + START + "A".repeat(10) + END));
+ decode(decoder, START + "next" + END);
+ assertEquals("next", session.getDecoderOutputQueue().poll());
+ }
+
+ @Test
+ void eachMessageOfOneReadHasTheLimit() throws Exception {
+ // the limit counts from the end bytes of the previous message, also
when several messages come in one read
+ ProtocolDecoder decoder = decoder(50);
+ String message = "A".repeat(49);
+
+ decode(decoder, START + message + END + START + message + END + START
+ message.substring(10));
+ decode(decoder, "A".repeat(10) + END);
+
+ assertEquals(message, session.getDecoderOutputQueue().poll());
+ assertEquals(message, session.getDecoderOutputQueue().poll());
+ assertEquals(message, session.getDecoderOutputQueue().poll());
+ assertNull(session.getDecoderOutputQueue().poll());
+ }
+
+ @Test
+ void theDataOfDecodedMessagesIsNotKept() throws Exception {
+ // reads that do not end with a message end: only the data of the
current message is kept for the next read
+ ProtocolDecoder decoder = decoder(50);
+ for (int i = 0; i < 1000; i++) {
+ decode(decoder, "A" + END + START + "message-" + i);
+ decode(decoder, END + START + "next-" + i);
+ }
+ assertTrue(sessionBufferSize() <= 50, "the session buffer has " +
sessionBufferSize() + " bytes");
+ // each read ends the message of the previous one (the first "A" has
no start byte), the last one stays open
+ assertEquals(1999, session.getDecoderOutputQueue().size());
+ assertEquals("message-0", session.getDecoderOutputQueue().poll());
+ assertEquals("next-0A", session.getDecoderOutputQueue().poll());
+ }
+
+ @Test
+ void endBytesWithoutAStartAreNotKept() throws Exception {
+ // end bytes without a start byte are ignored (with a WARN), and the
data before them is not kept
+ ProtocolDecoder decoder = decoder(50);
+ for (int i = 0; i < 100; i++) {
+ decode(decoder, "x" + END + "x");
+ }
+ assertTrue(sessionBufferSize() <= 50, "the session buffer has " +
sessionBufferSize() + " bytes");
+ decode(decoder, END + START + "next" + END);
+ assertEquals("next", session.getDecoderOutputQueue().poll());
+ }
+
+ @Test
+ // without the data kept, MINA's codec filter loops on the read for good:
fail instead
+ @Timeout(value = 10, unit = TimeUnit.SECONDS, threadMode =
Timeout.ThreadMode.SEPARATE_THREAD)
+ void aTransportWithoutFragmentationKeepsTheDataOfAnIncompleteMessage()
throws Exception {
+ // the test session reports no fragmentation, as a mina:udp or mina:vm
session: through the codec filter of
+ // MINA, a read with a message and the start of the next one gives
each message once
+ ProtocolCodecFilter filter = new ProtocolCodecFilter(new
HL7MLLPCodec());
+ List<Object> received = new ArrayList<>();
+ IoFilter.NextFilter next = (IoFilter.NextFilter)
Proxy.newProxyInstance(getClass().getClassLoader(),
+ new Class<?>[] { IoFilter.NextFilter.class }, (proxy, method,
args) -> {
+ if ("messageReceived".equals(method.getName()) ||
"exceptionCaught".equals(method.getName())) {
+ received.add(args[1]);
+ if (received.size() > 2) {
+ // the same message again: stop the loop of the
filter instead of filling the memory
+ throw new IllegalStateException("received " +
received);
+ }
+ }
+ return null;
+ });
+
+ filter.messageReceived(next, session,
+ IoBuffer.wrap((START + "A" + END + START +
"B").getBytes(StandardCharsets.ISO_8859_1)));
+ filter.messageReceived(next, session,
IoBuffer.wrap(END.getBytes(StandardCharsets.ISO_8859_1)));
+
+ assertEquals(List.of("A", "B"), received);
+ }
+
+ @Test
+ void startAndEndBytesAbove0x7F() throws Exception {
+ // the decoder compares the bytes it reads with the configured start
and end bytes unsigned
+ HL7MLLPConfig config = new HL7MLLPConfig();
+ config.setCharset(StandardCharsets.ISO_8859_1);
+ config.setStartByte((char) 0x8b);
+ config.setEndByte1((char) 0x9c);
+ config.setEndByte2((char) 0x8d);
+ config.setMaxFrameLength(50);
+ ProtocolDecoder decoder = new HL7MLLPDecoder(config);
+ String start = "\u008b";
+ String end = "\u009c\u008d";
+
+ decode(decoder, start + "A".repeat(49) + end);
+ assertEquals("A".repeat(49), session.getDecoderOutputQueue().poll());
+
+ // an end byte 1 that arrives without the end byte 2 is not counted yet
+ decode(decoder, start + "B".repeat(49) + "\u009c");
+ decode(decoder, "\u008d");
+ assertEquals("B".repeat(49), session.getDecoderOutputQueue().poll());
+
+ // a message over the limit is discarded up to its end bytes, and the
next message is decoded
+ assertThrows(ProtocolDecoderException.class, () -> decode(decoder,
start + "C".repeat(60)));
+ decode(decoder, "C".repeat(10) + end + start + "next" + end);
+ assertEquals("next", session.getDecoderOutputQueue().poll());
+ assertNull(session.getDecoderOutputQueue().poll());
+ }
+
+ @Test
+ void codecAppliesTheLimitToItsDecoders() throws Exception {
+ HL7MLLPCodec codec = new HL7MLLPCodec();
+ codec.setCharset(StandardCharsets.ISO_8859_1);
+ codec.setMaxFrameLength(50);
+ assertEquals(50, codec.getMaxFrameLength());
+ ProtocolDecoder decoder = codec.getDecoder(session);
+
+ decode(decoder, START + "A".repeat(49) + END);
+ assertEquals("A".repeat(49), session.getDecoderOutputQueue().poll());
+
+ assertThrows(ProtocolDecoderException.class, () -> decode(decoder,
START + "A".repeat(50) + END));
+ assertNull(session.getDecoderOutputQueue().poll());
+ }
+
+ @Test
+ void defaultIsNoLimit() throws Exception {
+ assertEquals(Integer.MAX_VALUE, new
HL7MLLPCodec().getMaxFrameLength());
+
+ String message = "C".repeat(1024 * 1024);
+ HL7MLLPCodec codec = new HL7MLLPCodec();
+ codec.setCharset(StandardCharsets.ISO_8859_1);
+ ProtocolDecoder decoder = codec.getDecoder(session);
+ decode(decoder, START + message.substring(1000));
+ decode(decoder, message.substring(0, 1000) + END);
+
+ assertEquals(message, session.getDecoderOutputQueue().poll());
+ }
+
+ @Test
+ void maxFrameLengthMustBePositive() {
+ HL7MLLPCodec codec = new HL7MLLPCodec();
+ assertThrows(IllegalArgumentException.class, () ->
codec.setMaxFrameLength(0));
+ assertThrows(IllegalArgumentException.class, () ->
codec.setMaxFrameLength(-1));
+ assertEquals(Integer.MAX_VALUE, codec.getMaxFrameLength());
+ }
+
+ /**
+ * The bytes that MINA keeps for the session between two reads (0 when it
keeps none).
+ */
+ private int sessionBufferSize() {
+ return
session.getAttributeKeys().stream().map(session::getAttribute).filter(IoBuffer.class::isInstance)
+ .mapToInt(b -> ((IoBuffer) b).position()).sum();
+ }
+
+ private void decode(ProtocolDecoder decoder, String data) throws Exception
{
+ decoder.decode(session,
IoBuffer.wrap(data.getBytes(StandardCharsets.ISO_8859_1)),
session.getDecoderOutput());
+ }
+
+ private static ProtocolDecoder decoder(int maxFrameLength) {
+ HL7MLLPConfig config = new HL7MLLPConfig();
+ config.setCharset(StandardCharsets.ISO_8859_1);
+ config.setMaxFrameLength(maxFrameLength);
+ return new HL7MLLPDecoder(config);
+ }
+}
diff --git
a/components/camel-hl7/src/test/java/org/apache/camel/component/hl7/HL7MLLPMinaMaxFrameLengthRouteTest.java
b/components/camel-hl7/src/test/java/org/apache/camel/component/hl7/HL7MLLPMinaMaxFrameLengthRouteTest.java
new file mode 100644
index 000000000000..ea08eeb4e9f1
--- /dev/null
+++
b/components/camel-hl7/src/test/java/org/apache/camel/component/hl7/HL7MLLPMinaMaxFrameLengthRouteTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.SocketException;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.camel.BindToRegistry;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * A MINA HL7 listener with a maxFrameLength refuses a longer message and
keeps serving other connections.
+ */
+class HL7MLLPMinaMaxFrameLengthRouteTest extends HL7TestSupport {
+
+ private static final String MESSAGE =
"MSH|^~\\&|MYSENDER|MYRECEIVER|MYAPPLICATION||200612211200||QRY^A19|1234|P|2.4";
+
+ @BindToRegistry("hl7codec")
+ HL7MLLPCodec addCodec() {
+ HL7MLLPCodec codec = new HL7MLLPCodec();
+ codec.setCharset("iso-8859-1");
+ codec.setMaxFrameLength(1024);
+ return codec;
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ public void configure() {
+ from("mina:tcp://127.0.0.1:" + getPort() +
"?sync=true&codec=#hl7codec")
+ .to("mock:result")
+ .setBody(constant("ACK"));
+ }
+ };
+ }
+
+ @Test
+ void messageOverTheLimitClosesItsConnectionAndOtherMessagesAreServed()
throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:result");
+ mock.expectedBodiesReceived(MESSAGE);
+
+ try (Socket socket = new Socket("127.0.0.1", getPort())) {
+ socket.setSoTimeout(10000);
+ OutputStream out = socket.getOutputStream();
+ // a message of 2000 bytes without the end bytes: the listener
does not wait for them
+ out.write(0x0b);
+ out.write("A".repeat(2000).getBytes(StandardCharsets.ISO_8859_1));
+ out.flush();
+ assertEquals(-1, readUntilClosed(socket.getInputStream()));
+ }
+
+ String reply = template.requestBody("mina:tcp://127.0.0.1:" +
getPort() + "?sync=true&codec=#hl7codec", MESSAGE,
+ String.class);
+ assertEquals("ACK", reply);
+
+ mock.assertIsSatisfied();
+ }
+
+ private static int readUntilClosed(InputStream in) throws IOException {
+ try {
+ return in.read();
+ } catch (SocketException e) {
+ // connection reset by the listener
+ return -1;
+ }
+ }
+}