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.
      *

Reply via email to