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 1d2cc581d94a camel-mllp: generate the acknowledgement timestamp
thread-safely (#29678)
1d2cc581d94a is described below
commit 1d2cc581d94a54b60878a492ba5bb7f6fbef3d30
Author: dhairya05 <[email protected]>
AuthorDate: Thu Nov 12 17:32:25 2026 +0530
camel-mllp: generate the acknowledgement timestamp thread-safely (#29678)
Hl7Util.generateAcknowledgementPayload wrote MSH-7 through a shared
SimpleDateFormat, with a TODO on the line saying so:
public static final SimpleDateFormat TIMESTAMP_FORMAT
= new SimpleDateFormat("yyyyMMddHHmmss.SSSZZZZ");
...
// 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());
SimpleDateFormat keeps its parse/format state in a Calendar field, so the
MLLP TCP server consumer - which acknowledges from one thread per accepted
connection - can interleave two format() calls and emit a timestamp built
from both, or an ArrayIndexOutOfBoundsException out of the shared Calendar.
TIMESTAMP_FORMAT becomes a DateTimeFormatter, which is immutable and
thread safe. The pattern is "yyyyMMddHHmmss.SSSZ": in DateTimeFormatter a
single Z is the RFC 822 offset (+0100) that SimpleDateFormat wrote for
ZZZZ, whereas ZZZZ in DateTimeFormatter would be the localized GMT+01:00
form. Checked against the old formatter over several instants in
America/New_York, Europe/Paris, UTC, Asia/Kolkata and Pacific/Chatham,
across DST boundaries and 30- and 45-minute offsets: the output is
identical.
---
.../camel/component/mllp/internal/Hl7Util.java | 14 ++++---
.../camel/component/mllp/internal/Hl7UtilTest.java | 43 ++++++++++++++++++++++
2 files changed, 52 insertions(+), 5 deletions(-)
diff --git
a/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/internal/Hl7Util.java
b/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/internal/Hl7Util.java
index 5e5c8a3c0c4a..8e0aaa03141b 100644
---
a/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/internal/Hl7Util.java
+++
b/components/camel-mllp/src/main/java/org/apache/camel/component/mllp/internal/Hl7Util.java
@@ -17,8 +17,8 @@
package org.apache.camel.component.mllp.internal;
import java.nio.charset.Charset;
-import java.text.SimpleDateFormat;
-import java.util.Date;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
@@ -41,7 +41,12 @@ public final class Hl7Util {
public static final Map<Character, String> CHARACTER_REPLACEMENTS;
- public static final SimpleDateFormat TIMESTAMP_FORMAT = new
SimpleDateFormat("yyyyMMddHHmmss.SSSZZZZ");
+ /**
+ * The MSH-7 format of a generated acknowledgement. {@link
DateTimeFormatter} is immutable and thread safe, unlike
+ * the {@link java.text.SimpleDateFormat} this used to be; the pattern
letter {@code Z} is the RFC 822 offset
+ * ({@code +0100}), which is what {@code SimpleDateFormat} wrote for
{@code ZZZZ}.
+ */
+ public static final DateTimeFormatter TIMESTAMP_FORMAT =
DateTimeFormatter.ofPattern("yyyyMMddHHmmss.SSSZ");
static final int STRING_BUFFER_PAD_SIZE = 100;
@@ -283,8 +288,7 @@ public final class Hl7Util {
// 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());
+
mllpSocketBuffer.write(TIMESTAMP_FORMAT.format(ZonedDateTime.now()).getBytes());
// Don't copy MSH-8
mllpSocketBuffer.write(fieldSeparator);
diff --git
a/components/camel-mllp/src/test/java/org/apache/camel/component/mllp/internal/Hl7UtilTest.java
b/components/camel-mllp/src/test/java/org/apache/camel/component/mllp/internal/Hl7UtilTest.java
index ca733f0dbeaa..bab64fc826e5 100644
---
a/components/camel-mllp/src/test/java/org/apache/camel/component/mllp/internal/Hl7UtilTest.java
+++
b/components/camel-mllp/src/test/java/org/apache/camel/component/mllp/internal/Hl7UtilTest.java
@@ -19,6 +19,10 @@ package org.apache.camel.component.mllp.internal;
import java.io.ByteArrayOutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.regex.Pattern;
import org.apache.camel.component.mllp.MllpAcknowledgementGenerationException;
import org.apache.camel.component.mllp.MllpProtocolConstants;
@@ -33,6 +37,7 @@ import static org.hamcrest.Matchers.matchesPattern;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class Hl7UtilTest {
@@ -233,6 +238,44 @@ public class Hl7UtilTest {
assertThat(actualMsh7Field,
matchesPattern("\\d{14}\\.\\d{3}[+-]\\d{4}"));
}
+ @Test
+ public void testGenerateAcknowledgementPayloadTimestampIsThreadSafe()
throws Exception {
+ final Pattern msh7 = Pattern.compile("\\d{14}\\.\\d{3}[+-]\\d{4}");
+ final int threadCount = 8;
+ final int acknowledgementsPerThread = 500;
+ final List<String> malformed = new CopyOnWriteArrayList<>();
+ final List<Throwable> failures = new CopyOnWriteArrayList<>();
+
+ List<Thread> threads = new ArrayList<>(threadCount);
+ for (int i = 0; i < threadCount; i++) {
+ Thread thread = new Thread(() -> {
+ try {
+ for (int j = 0; j < acknowledgementsPerThread; j++) {
+ MllpSocketBuffer buffer = new MllpSocketBuffer(new
MllpEndpointStub());
+ hl7util.generateAcknowledgementPayload(buffer,
TEST_MESSAGE.getBytes(), "AA");
+
+ String field = buffer.toString().split("\\|")[6];
+ if (!msh7.matcher(field).matches()) {
+ malformed.add(field);
+ }
+ }
+ } catch (Throwable t) {
+ failures.add(t);
+ }
+ });
+ threads.add(thread);
+ thread.start();
+ }
+ for (Thread thread : threads) {
+ thread.join();
+ }
+
+ assertTrue(failures.isEmpty(), () -> "generating an acknowledgement
threw " + failures.get(0));
+ assertTrue(malformed.isEmpty(),
+ () -> malformed.size() + " of " + threadCount *
acknowledgementsPerThread
+ + " acknowledgements had a corrupted MSH-7, such as " +
malformed.get(0));
+ }
+
/**
* Description of test.
*