• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

synadia-io / nats.java.v3 / 37648054681

07 Oct 2026 03:55PM UTC coverage: 89.174% (-2.1%) from 91.29%
37648054681

push

github

scottf
NatsConnectionV3 (default) with OutboundBuffer, classic fixes, build-variants CI

- NatsConnectionV3 extends NatsConnection, selected by ConnectionImplementation (V3 default, Classic available via option, property or -PconnectionImplementation)
- OutboundBuffer: control lane + bounded data blocks; SUB control line limit check; kept-block limits from the first server's max payload
- Classic fixes: protocol buffer sizing, tryingToConnect CAS, lock-free pending getters, sendPing pong future cleanup, per-run reader/writer flag
- build-variants.yml replaces build-windows.yml
- Plans: connection V3, active/passive V3, testing.java overview, V2 bugs from review

4596 of 5421 branches covered (84.78%)

Branch coverage included in aggregate %.

1027 of 1185 new or added lines in 12 files covered. (86.67%)

161 existing lines in 4 files now uncovered.

11532 of 12665 relevant lines covered (91.05%)

4.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.68
/core/src/main/java/io/synadia/client/impl/NatsConnectionReader.java
1
package io.synadia.client.impl;
2

3
import io.synadia.client.Message;
4
import io.synadia.client.ReadListener;
5
import io.synadia.client.utils.IncomingHeadersProcessor;
6

7
import java.io.IOException;
8
import java.nio.ByteBuffer;
9
import java.nio.CharBuffer;
10
import java.nio.charset.StandardCharsets;
11
import java.util.concurrent.CancellationException;
12
import java.util.concurrent.CompletableFuture;
13
import java.util.concurrent.ExecutionException;
14
import java.util.concurrent.Future;
15
import java.util.concurrent.atomic.AtomicBoolean;
16

17
import static io.synadia.client.utils.NatsConstants.*;
18

19
/**
20
 * The read loop for a connection. It runs on its own thread, pulls bytes off the socket and
21
 * advances a small state machine that gathers a protocol op, then its protocol line, then any
22
 * headers and payload, handing each completed message to the connection for delivery.
23
 */
24
public class NatsConnectionReader implements Runnable {
25

26
    enum Mode {
3 ✔
27
        GATHER_OP,
6 ✔
28
        GATHER_PROTO,
6 ✔
29
        GATHER_MSG_HMSG_PROTO,
6 ✔
30
        PARSE_PROTO,
6 ✔
31
        GATHER_HEADERS,
6 ✔
32
        GATHER_DATA
6 ✔
33
    }
34

35
    // volatile (was final) so a live reader can be repointed at a different connection - the reader
36
    // thread reads this on every delivered message and must see the new target. Repointing is done
37
    // through setConnection(...) rather than direct field access, so the field stays private; see
38
    // NatsConnection.setReaderConnection(...) for the entry point
39
    private volatile NatsConnection connection;
40

41
    private ByteBuffer protocolBuffer; // use a byte buffer to assist character decoding
42

43
    private boolean gotCR;
44
    
45
    private String op;
46
    private final char[] opArray;
47
    private int opPos;
48

49
    private final char[] msgLineChars;
50
    private int msgLinePosition;
51

52
    private Mode mode;
53

54
    private IncomingMessageFactory incoming;
55
    private byte[] msgHeaders;
56
    private byte[] msgData;
57
    private int msgHeadersPosition;
58
    private int msgDataPosition;
59

60
    private final byte[] buffer;
61
    private int bufferPosition;
62

63
    private Future<Boolean> stopped;
64
    private Future<DataPort> dataPortFuture;
65
    private DataPort dataPort;
66
    // A new flag per start(). The thread started by start() checks and clears only the flag it was
67
    // started with, so a late thread from a previous socket cannot stop or disturb the current one.
68
    private volatile AtomicBoolean running;
69

70
    private final boolean utf8Mode;
71
    // The listener the reader thread invokes: either a no-op (no ReadListener configured) or a wrapper
72
    // that dispatches on the CURRENT connection (read from the volatile connection field at call time).
73
    // volatile because the reader thread reads it and the setConnection caller thread may replace it.
74
    private volatile ReadListener readListener;
75
    // The raw ReadListener (from Options) the current wrapper was built from - null when the no-op is in
76
    // place. Used by refreshReadListener to skip a needless rebuild when a repoint doesn't change it.
77
    // Only touched by the constructing / repointing thread, never the reader thread.
78
    private ReadListener currentUserRl;
79

80
    NatsConnectionReader(NatsConnection connection) {
2 ✔
81
        this.connection = connection;
3 ✔
82

83
        this.running = new AtomicBoolean(false);
6 ✔
84
        this.stopped = new CompletableFuture<>();
5 ✔
85
        ((CompletableFuture<Boolean>)this.stopped).complete(Boolean.TRUE); // we are stopped on creation
6 ✔
86

87
        this.protocolBuffer = ByteBuffer.allocate(this.connection.getOptions().getMaxControlLine());
7 ✔
88
        this.msgLineChars = new char[this.connection.getOptions().getMaxControlLine()];
7 ✔
89
        this.opArray = new char[MAX_PROTOCOL_RECEIVE_OP_LENGTH];
4 ✔
90
        this.buffer = new byte[connection.getOptions().getBufferSize()];
6 ✔
91
        this.bufferPosition = 0;
3 ✔
92

93
        this.utf8Mode = connection.getOptions().supportUTF8Subjects();
5 ✔
94

95
        refreshReadListener(connection);
3 ✔
96
    }
1 ✔
97

98
    void setReadListener(ReadListener rl) {
99
        // keep currentUserRl in step with what is actually installed, so a later repoint compares against
100
        // this listener rather than the previous connection's Options (see refreshReadListener)
101
        currentUserRl = rl;
3 ✔
102
        if (rl == null) {
2 !
103
            readListener = new ReadListener() {};
×
104
        }
105
        else {
106
            readListener = new ReadListener() {
16 ✔
107
                @Override
108
                public void protocol(String op, String text) {
109
                    connection.makeCallback(() -> rl.protocol(op, text));
14 ✔
110
                }
1 ✔
111

112
                @Override
113
                public void message(String op, Message message) {
114
                    connection.makeCallback(() -> rl.message(op, message));
×
115
                }
×
116
            };
117
        }
118
    }
1 ✔
119

120
    // (Re)derive the readListener from the connection's Options, but replace it ONLY when the configured
121
    // ReadListener actually changed - so a repoint that doesn't change it costs nothing. The wrapper reads
122
    // the current connection from the volatile field at dispatch time (not a captured reference), so when
123
    // the source listener is unchanged the existing wrapper (or no-op) is already correct for the new
124
    // connection and is kept as-is.
125
    //   - first call (construction): readListener is still null, so we always build.
126
    //   - repoint, old null + new null: same (no-op already installed) - keep it.
127
    //   - repoint, old == new (same instance): the wrapper is fine - keep it.
128
    //   - otherwise: build the matching wrapper (or no-op) and remember the new source.
129
    private void refreshReadListener(NatsConnection connection) {
130
        final ReadListener newSuppliedUserRl = connection.getOptions().getReadListener();
4 ✔
131
        if (readListener != null && newSuppliedUserRl == currentUserRl) {
7 ✔
132
            return;
1 ✔
133
        }
134
        currentUserRl = newSuppliedUserRl;
3 ✔
135
        if (newSuppliedUserRl == null) {
2 ✔
136
            readListener = new ReadListener() {};
10 ✔
137
        }
138
        else {
139
            readListener = new ReadListener() {
16 ✔
140
                @Override
141
                public void protocol(String op, String text) {
142
                    NatsConnectionReader.this.connection.makeCallback(() -> newSuppliedUserRl.protocol(op, text));
14 ✔
143
                }
1 ✔
144

145
                @Override
146
                public void message(String op, Message message) {
147
                    NatsConnectionReader.this.connection.makeCallback(() -> newSuppliedUserRl.message(op, message));
14 ✔
148
                }
1 ✔
149
            };
150
        }
151
    }
1 ✔
152

153
    // test access only - the listener the reader thread would invoke (no-op or wrapper)
154
    ReadListener readListener() {
155
        return readListener;
3 ✔
156
    }
157

158
    // Repoint this reader at a different connection. The connection field is volatile, so the reader
159
    // thread sees the new target the next time it reads it, and the ReadListener is re-derived from the
160
    // new connection's Options only when it actually changed (see refreshReadListener).
161
    //
162
    // What follows the repoint: everything read from the connection field on use - message delivery,
163
    // statistics, protocol handling (OK/ERR/PONG/INFO), and the re-derived ReadListener. What does NOT
164
    // follow: the wire-parse parameters fixed at construction - the read buffer size, the max-control-line
165
    // buffers (msgLineChars / protocolBuffer), and utf8 subject mode. Those hold in-flight parse state and
166
    // are used in tight loops by the reader thread; swapping them from another thread mid-parse would
167
    // corrupt the message being decoded, so they are intentionally left as constructed. A repoint is
168
    // therefore only fully safe across connections whose parse-affecting Options (buffer size, max control
169
    // line, utf8 subjects) match; otherwise the reader keeps parsing per its original Options.
170
    //
171
    // INVARIANT: volatile gives visibility, NOT atomicity across a multi-step message parse. This method
172
    // does NOT require the reader to be stopped first - a caller may repoint a running reader - so if a
173
    // repoint lands mid-message it can straddle both connections (e.g. read stats registered on the
174
    // previous connection, delivery on the new one). A caller needing a clean handoff must ensure the
175
    // reader is between messages, or accept the split. When the two connections share the same underlying
176
    // socket, delivering to the new connection is correct and only a read-byte stat is split.
177
    protected void setConnection(NatsConnection connection) {
178
        this.connection = connection;
3 ✔
179
        refreshReadListener(connection);
3 ✔
180
    }
1 ✔
181

182
    // Should only be called if the current thread has exited.
183
    // Use the Future from stop() to determine if it is ok to call this.
184
    // This method resets that future so mistiming can result in badness.
185
    void start(Future<DataPort> dataPortFuture) {
186
        this.dataPortFuture = dataPortFuture;
3 ✔
187
        AtomicBoolean runFlag = new AtomicBoolean(true);
5 ✔
188
        this.running = runFlag;
3 ✔
189
        this.stopped = connection.getReaderExecutor().submit(() -> run(runFlag), Boolean.TRUE);
14 ✔
190
    }
1 ✔
191

192
    Future<Boolean> stop() {
193
        return stop(true);
4 ✔
194
    }
195

196
    // May be called several times on an error.
197
    // Returns a future that is completed when the thread completes, not when this
198
    // method does.
199
    Future<Boolean> stop(boolean shutdownDataPort) {
200
        AtomicBoolean runFlag = running;
3 ✔
201
        if (runFlag.get()) {
3 ✔
202
            runFlag.set(false);
3 ✔
203
            if (shutdownDataPort && dataPort != null) {
5 !
204
                try {
205
                    dataPort.shutdownInput();
3 ✔
206
                }
207
                catch (IOException e) {
×
208
                    // we don't care, we are shutting down anyway
209
                }
1 ✔
210
            }
211
        }
212
        return stopped;
3 ✔
213
    }
214

215
    boolean isRunning() {
216
        return running.get();
4 ✔
217
    }
218

219
    @Override
220
    public void run() {
NEW
221
        run(running);
×
NEW
222
    }
×
223

224
    private void run(AtomicBoolean runFlag) {
225
        try {
226
            dataPort = this.dataPortFuture.get(); // Will wait for the future to complete
6 ✔
227
            this.mode = Mode.GATHER_OP;
3 ✔
228
            this.gotCR = false;
3 ✔
229
            this.opPos = 0;
3 ✔
230

231
            while (runFlag.get() && !Thread.interrupted()) {
5 !
232
                this.bufferPosition = 0;
3 ✔
233
                int bytesRead = dataPort.read(this.buffer, 0, this.buffer.length);
10 ✔
234

235
                if (bytesRead > 0) {
2 ✔
236
                    connection.getStatisticsCollector().registerRead(bytesRead);
6 ✔
237

238
                    while (this.bufferPosition < bytesRead) {
4 ✔
239
                        if (this.mode == Mode.GATHER_OP) {
4 ✔
240
                            this.gatherOp(bytesRead);
4 ✔
241
                        }
242
                        else if (this.mode == Mode.GATHER_MSG_HMSG_PROTO) {
4 ✔
243
                            if (this.utf8Mode) {
3 ✔
244
                                this.gatherProtocol(bytesRead);
4 ✔
245
                            } else {
246
                                this.gatherMessageProtocol(bytesRead);
4 ✔
247
                            }
248
                        }
249
                        else if (this.mode == Mode.GATHER_PROTO) {
4 ✔
250
                            this.gatherProtocol(bytesRead);
4 ✔
251
                        }
252
                        else if (this.mode == Mode.GATHER_HEADERS) {
4 ✔
253
                            this.gatherHeaders(bytesRead);
4 ✔
254
                        }
255
                        else {  // Mode.GATHER_DATA
256
                            this.gatherMessageData(bytesRead);
3 ✔
257
                        }
258

259
                        if (this.mode == Mode.PARSE_PROTO) { // Could be the end of the read
4 ✔
260
                            this.parseProtocolMessage();
2 ✔
261
                            this.protocolBuffer.clear();
5 ✔
262
                        }
263
                    }
264
                } else if (bytesRead < 0) {
2 !
265
                    throw new IOException("Read channel closed.");
5 ✔
266
                } else {
267
                    this.connection.getStatisticsCollector().registerRead(bytesRead); // track the 0
×
268
                }
269
            }
1 ✔
270
        } catch (IOException io) {
1 ✔
271
            // if already not running, an IOE is not unreasonable in a transition state
272
            if (runFlag.get()) {
3 ✔
273
                this.connection.handleCommunicationIssue(this, io);
5 ✔
274
            }
275
        } catch (CancellationException | ExecutionException ex) {
×
276
            // Exit
277
        } catch (InterruptedException ex) {
×
278
            // Exit
279
            Thread.currentThread().interrupt();
×
280
        } finally {
281
            runFlag.set(false);
3 ✔
282
            // Clear the buffers, since they are only used inside this try/catch
283
            // We will reuse later
284
            this.protocolBuffer.clear();
4 ✔
285
        }
286
    }
1 ✔
287

288
    // Gather the op, either up to the first space or the first carriage return.
289
    void gatherOp(int maxPos) throws IOException {
290
        try {
291
            while(this.bufferPosition < maxPos) {
4 ✔
292
                byte b = this.buffer[this.bufferPosition];
6 ✔
293
                this.bufferPosition++;
6 ✔
294

295
                if (gotCR) {
3 ✔
296
                    if (b == LF) { // Got CRLF, jump to parsing
3 !
297
                        this.op = opFor(opArray, opPos);
7 ✔
298
                        this.gotCR = false;
3 ✔
299
                        this.opPos = 0;
3 ✔
300
                        this.mode = Mode.PARSE_PROTO;
3 ✔
301
                        break;
1 ✔
302
                    } else {
303
                        throw new IllegalStateException("Bad socket data, no LF after CR");
×
304
                    }
305
                } else if (b == SP || b == TAB) { // Got a space, get the rest of the protocol line
6 ✔
306
                    this.op = opFor(opArray, opPos);
7 ✔
307
                    this.opPos = 0;
3 ✔
308
                    if (this.op.equals(OP_MSG) || this.op.equals(OP_HMSG)) {
10 ✔
309
                        this.msgLinePosition = 0;
3 ✔
310
                        this.mode = Mode.GATHER_MSG_HMSG_PROTO;
4 ✔
311
                    } else {
312
                        this.mode = Mode.GATHER_PROTO;
3 ✔
313
                    }
314
                    break;
1 ✔
315
                } else if (b == CR) {
3 ✔
316
                    this.gotCR = true;
4 ✔
317
                } else {
318
                    this.opArray[opPos] = (char) b;
7 ✔
319
                    this.opPos++;
6 ✔
320
                }
321
            }
1 ✔
322
        }
323
        catch (ArrayIndexOutOfBoundsException | IllegalStateException | NumberFormatException | NullPointerException ex) {
1 ✔
324
            throw new IOException("Gather Operation", ex);
6 ✔
325
        }
1 ✔
326
    }
1 ✔
327

328
    // Stores the message protocol line in a char buffer that will be read for subject, reply
329
    void gatherMessageProtocol(int maxPos) throws IOException {
330
        try {
331
            while(this.bufferPosition < maxPos) {
4 ✔
332
                byte b = this.buffer[this.bufferPosition];
6 ✔
333
                this.bufferPosition++;
6 ✔
334

335
                if (gotCR) {
3 ✔
336
                    if (b == LF) {
3 ✔
337
                        this.mode = Mode.PARSE_PROTO;
3 ✔
338
                        this.gotCR = false;
3 ✔
339
                        break;
1 ✔
340
                    } else {
341
                        throw new IllegalStateException("Bad socket data, no LF after CR");
5 ✔
342
                    }
343
                } else if (b == CR) {
3 ✔
344
                    this.gotCR = true;
4 ✔
345
                } else {
346
                    if (this.msgLinePosition >= this.msgLineChars.length) {
6 !
347
                        throw new IllegalStateException("Protocol line is too long");
×
348
                    }
349
                    this.msgLineChars[this.msgLinePosition] = (char) b; // Assumes ascii, as per protocol doc
7 ✔
350
                    this.msgLinePosition++;
6 ✔
351
                }
352
            }
1 ✔
353
        }
354
        catch (IllegalStateException | NumberFormatException | NullPointerException ex) {
1 ✔
355
            throw new IOException("Gather Message", ex);
6 ✔
356
        }
1 ✔
357
    }
1 ✔
358

359
    // Gather bytes for a protocol line
360
    void gatherProtocol(int maxPos) throws IOException {
361
        // protocol buffer has max capacity, shouldn't need resizing
362
        try {
363
            while(this.bufferPosition < maxPos) {
4 ✔
364
                byte b = this.buffer[this.bufferPosition];
6 ✔
365
                this.bufferPosition++;
6 ✔
366

367
                if (gotCR) {
3 ✔
368
                    if (b == LF) {
3 !
369
                        this.protocolBuffer.flip();
4 ✔
370
                        this.mode = Mode.PARSE_PROTO;
3 ✔
371
                        this.gotCR = false;
3 ✔
372
                        break;
1 ✔
373
                    } else {
374
                        throw new IllegalStateException("Bad socket data, no LF after CR");
×
375
                    }
376
                } else if (b == CR) {
3 ✔
377
                    this.gotCR = true;
4 ✔
378
                } else {
379
                    if (!protocolBuffer.hasRemaining()) {
4 ✔
380
                        this.protocolBuffer = this.connection.enlargeBuffer(this.protocolBuffer); // just double it
7 ✔
381
                    }
382
                    this.protocolBuffer.put(b);
5 ✔
383
                }
384
            }
1 ✔
385
        }
386
        catch (IllegalStateException | NumberFormatException | NullPointerException ex) {
×
387
            throw new IOException("Gather Protocol", ex);
×
388
        }
1 ✔
389
    }
1 ✔
390

391
    void gatherHeaders(int maxPos) throws IOException {
392
        try {
393
            while(this.bufferPosition < maxPos) {
4 !
394
                int possible = maxPos - this.bufferPosition;
5 ✔
395
                int want = msgHeaders.length - msgHeadersPosition;
7 ✔
396

397
                // Grab all we can, until we get the necessary number of bytes
398
                if (want > 0 && want <= possible) {
5 !
399
                    System.arraycopy(this.buffer, this.bufferPosition, this.msgHeaders, this.msgHeadersPosition, want);
10 ✔
400
                    msgHeadersPosition += want;
6 ✔
401
                    this.bufferPosition += want;
6 ✔
402
                    continue;
1 ✔
403
                } else if (want > 0) {
2 !
404
                    System.arraycopy(this.buffer, this.bufferPosition, this.msgHeaders, this.msgHeadersPosition, possible);
×
405
                    msgHeadersPosition += possible;
×
406
                    this.bufferPosition += possible;
×
407
                    continue;
×
408
                }
409

410
                if (msgHeadersPosition == msgHeaders.length) {
6 !
411
                    incoming.setHeaders(new IncomingHeadersProcessor(msgHeaders));
8 ✔
412
                    msgHeaders = null;
3 ✔
413
                    msgHeadersPosition = -1;
3 ✔
414
                    this.mode = Mode.GATHER_DATA;
3 ✔
415
                    break;
1 ✔
416
                } else {
417
                    throw new IllegalStateException("Bad socket data, headers do not match expected length");
×
418
                }
419
            }
420
        }
421
        catch (IllegalStateException | NullPointerException ex) {
×
422
            throw new IOException("Gather Header", ex);
×
423
        }
1 ✔
424
    }
1 ✔
425

426
    // Gather bytes for a message body into a byte array that is then
427
    // given to the message object
428
    void gatherMessageData(int maxPos) throws IOException {
429
        try {
430
            while(this.bufferPosition < maxPos) {
4 ✔
431
                int possible = maxPos - this.bufferPosition;
5 ✔
432
                int want = msgData.length - msgDataPosition;
7 ✔
433

434
                // Grab all we can, until we get to the CR/LF
435
                if (want > 0 && want <= possible) {
5 ✔
436
                    System.arraycopy(this.buffer, this.bufferPosition, this.msgData, this.msgDataPosition, want);
10 ✔
437
                    msgDataPosition += want;
6 ✔
438
                    this.bufferPosition += want;
6 ✔
439
                    continue;
1 ✔
440
                } else if (want > 0) {
2 ✔
441
                    System.arraycopy(this.buffer, this.bufferPosition, this.msgData, this.msgDataPosition, possible);
10 ✔
442
                    msgDataPosition += possible;
6 ✔
443
                    this.bufferPosition += possible;
6 ✔
444
                    continue;
1 ✔
445
                }
446

447
                byte b = this.buffer[this.bufferPosition];
6 ✔
448
                this.bufferPosition++;
6 ✔
449

450
                if (gotCR) {
3 ✔
451
                    if (b == LF) {
3 ✔
452
                        incoming.setData(msgData);
5 ✔
453
                        NatsMessage m = incoming.getMessage();
4 ✔
454
                        this.connection.deliverMessage(m);
4 ✔
455
                        readListener.message(op, m);
6 ✔
456
                        msgData = null;
3 ✔
457
                        msgDataPosition = 0;
3 ✔
458
                        incoming = null;
3 ✔
459
                        gotCR = false;
3 ✔
460
                        this.op = UNKNOWN_OP;
3 ✔
461
                        this.mode = Mode.GATHER_OP;
3 ✔
462
                        break;
1 ✔
463
                    } else {
464
                        throw new IllegalStateException("Bad socket data, no LF after CR");
5 ✔
465
                    }
466
                } else if (b == CR) {
3 ✔
467
                    gotCR = true;
4 ✔
468
                } else {
469
                    throw new IllegalStateException("Bad socket data, no CRLF after data");
5 ✔
470
                }
471
            }
1 ✔
472
        }
473
        catch (IllegalStateException | NullPointerException ex) {
1 ✔
474
            throw new IOException("Gather Message Data", ex);
6 ✔
475
        }
1 ✔
476
    }
1 ✔
477

478
    /**
479
     * Take the next whitespace delimited element from the protocol line currently being parsed,
480
     * advancing the line position past it and past the delimiter.
481
     * @param max the position in the line to stop at, normally the length of the gathered line
482
     * @return the element, or null if the line position is already at or past max
483
     */
484
    public String grabNextMessageLineElement(int max) {
485
        if (this.msgLinePosition >= max) {
4 ✔
486
            return null;
2 ✔
487
        }
488

489
        int start = this.msgLinePosition;
3 ✔
490

491
        while (this.msgLinePosition < max) {
4 ✔
492
            char c = this.msgLineChars[this.msgLinePosition];
6 ✔
493
            this.msgLinePosition++;
6 ✔
494

495
            if (c == SP || c == TAB) {
6 ✔
496
                return new String(this.msgLineChars, start, this.msgLinePosition - start -1); //don't grab the space, avoid an intermediate char sequence
13 ✔
497
            }
498
        }
1 ✔
499

500
        return new String(this.msgLineChars, start, this.msgLinePosition-start);
11 ✔
501
    }
502

503
    static String opFor(char[] chars, int length) {
504
        if (length == 3) {
3 ✔
505
            if ((chars[0] == 'M' || chars[0] == 'm') &&
30 ✔
506
                        (chars[1] == 'S' || chars[1] == 's') && 
507
                        (chars[2] == 'G' || chars[2] == 'g')) {
508
                return OP_MSG;
2 ✔
509
            } else if (chars[0] == '+' && 
25 ✔
510
                (chars[1] == 'O' || chars[1] == 'o') && 
511
                (chars[2] == 'K' || chars[2] == 'k')) {
512
                return OP_OK;
2 ✔
513
            } else {
514
                return UNKNOWN_OP;
2 ✔
515
            }
516
        } else if (length == 4) { // do them in a unique order for uniqueness when possible to branch asap
3 ✔
517
            if ((chars[1] == 'I' || chars[1] == 'i') && 
40 ✔
518
                    (chars[0] == 'P' || chars[0] == 'p') && 
519
                    (chars[2] == 'N' || chars[2] == 'n') &&
520
                    (chars[3] == 'G' || chars[3] == 'g')) {
521
                return OP_PING;
2 ✔
522
            } else if ((chars[1] == 'O' || chars[1] == 'o') && 
40 ✔
523
                        (chars[0] == 'P' || chars[0] == 'p') && 
524
                        (chars[2] == 'N' || chars[2] == 'n') &&
525
                        (chars[3] == 'G' || chars[3] == 'g')) {
526
                return OP_PONG;
2 ✔
527
            } else if (chars[0] == '-' && 
35 ✔
528
                        (chars[1] == 'E' || chars[1] == 'e') &&
529
                        (chars[2] == 'R' || chars[2] == 'r') && 
530
                        (chars[3] == 'R' || chars[3] == 'r')) {
531
                return OP_ERR;
2 ✔
532
            } else if ((chars[0] == 'I' || chars[0] == 'i') &&
40 ✔
533
                    (chars[1] == 'N' || chars[1] == 'n') &&
534
                    (chars[2] == 'F' || chars[2] == 'f') &&
535
                    (chars[3] == 'O' || chars[3] == 'o')) {
536
                return OP_INFO;
2 ✔
537
            } else if ((chars[0] == 'H' || chars[0] == 'h') &&
40 ✔
538
                    (chars[1] == 'M' || chars[1] == 'm') &&
539
                    (chars[2] == 'S' || chars[2] == 's') &&
540
                    (chars[3] == 'G' || chars[3] == 'g')) {
541
                return OP_HMSG;
2 ✔
542
            }  else {
543
                return UNKNOWN_OP;
2 ✔
544
            }
545
        } else {
546
            return UNKNOWN_OP;
2 ✔
547
        }
548
    }
549

550
    private static final int[] TENS = new int[] { 1, 10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000, 100_000_000, 1_000_000_000};
44 ✔
551

552
    /**
553
     * Parse an unsigned decimal length from a protocol line. Faster than Integer.parseInt because it
554
     * accepts only digits, no sign and no whitespace, and it uses a lookup table of powers of ten.
555
     * @param s the digits to parse
556
     * @return the value
557
     * @throws NumberFormatException if a character is not a digit, or there are more digits than an int can hold
558
     */
559
    public static int parseLength(String s) throws NumberFormatException {
560
        int length = s.length();
3 ✔
561
        int retVal = 0;
2 ✔
562

563
        if (length > TENS.length) {
4 ✔
564
            throw new NumberFormatException("Long in message length \"" + s + "\" "+length+" > "+TENS.length);
9 ✔
565
        }
566
        
567
        for (int i=length-1;i>=0;i--) {
8 ✔
568
            char c = s.charAt(i);
4 ✔
569
            int d = (c - '0');
4 ✔
570

571
            if (d>9) {
3 ✔
572
                throw new NumberFormatException("Invalid char in message length '" + c + "'");
6 ✔
573
            }
574

575
            retVal += d * TENS[length - i - 1];
12 ✔
576
        }
577

578
        return retVal;
2 ✔
579
    }
580

581
    void parseProtocolMessage() throws IOException {
582
        try {
583
            switch (this.op) {
9 ✔
584
                case OP_MSG:
585
                    int protocolLength = this.msgLinePosition; //This is just after the last character
3 ✔
586
                    int protocolLineLength = protocolLength + 4; // 4 for the "MSG "
4 ✔
587

588
                    if (this.utf8Mode) {
3 ✔
589
                        protocolLineLength = protocolBuffer.remaining() + 4;
6 ✔
590

591
                        CharBuffer buff = StandardCharsets.UTF_8.decode(protocolBuffer);
5 ✔
592
                        protocolLength = buff.remaining();
3 ✔
593
                        buff.get(this.msgLineChars, 0, protocolLength);
7 ✔
594
                    }
595

596
                    this.msgLinePosition = 0;
3 ✔
597
                    String subject = grabNextMessageLineElement(protocolLength);
4 ✔
598
                    String sid = grabNextMessageLineElement(protocolLength);
4 ✔
599
                    String replyTo = grabNextMessageLineElement(protocolLength);
4 ✔
600
                    String lengthChars;
601

602
                    if (this.msgLinePosition < protocolLength) {
4 ✔
603
                        lengthChars = grabNextMessageLineElement(protocolLength);
5 ✔
604
                    } else {
605
                        lengthChars = replyTo;
2 ✔
606
                        replyTo = null;
2 ✔
607
                    }
608

609
                    if (subject == null || subject.isEmpty() || sid == null || sid.isEmpty() || lengthChars == null) {
12 !
610
                        throw new IllegalStateException("Bad MSG control line, missing required fields");
5 ✔
611
                    }
612

613
                    int incomingLength = parseLength(lengthChars);
3 ✔
614

615
                    this.incoming = new IncomingMessageFactory(sid, subject, replyTo, protocolLineLength);
9 ✔
616
                    this.mode = Mode.GATHER_DATA;
3 ✔
617
                    this.msgData = new byte[incomingLength];
4 ✔
618
                    this.msgDataPosition = 0;
3 ✔
619
                    this.msgLinePosition = 0;
3 ✔
620
                    break;
1 ✔
621
                case OP_HMSG:
622
                    int hProtocolLength = this.msgLinePosition; //This is just after the last character
3 ✔
623
                    int hProtocolLineLength = hProtocolLength + 5; // 5 for the "HMSG "
4 ✔
624

625
                    if (this.utf8Mode) {
3 !
626
                        hProtocolLineLength = protocolBuffer.remaining() + 5;
×
627

628
                        CharBuffer buff = StandardCharsets.UTF_8.decode(protocolBuffer);
×
629
                        hProtocolLength = buff.remaining();
×
630
                        buff.get(this.msgLineChars, 0, hProtocolLength);
×
631
                    }
632

633
                    this.msgLinePosition = 0;
3 ✔
634
                    String hSubject = grabNextMessageLineElement(hProtocolLength);
4 ✔
635
                    String hSid = grabNextMessageLineElement(hProtocolLength);
4 ✔
636
                    String replyToOrHdrLen = grabNextMessageLineElement(hProtocolLength);
4 ✔
637
                    String hdrLenOrTotLen = grabNextMessageLineElement(hProtocolLength);
4 ✔
638

639
                    String hReplyTo = null;
2 ✔
640
                    int hdrLen;
641
                    int totLen;
642

643
                    // if there is more it must be replyTo hdrLen totLen instead of just hdrLen totLen
644
                    if (this.msgLinePosition < hProtocolLength) {
4 ✔
645
                        hReplyTo = replyToOrHdrLen;
2 ✔
646
                        hdrLen = parseLength(hdrLenOrTotLen);
3 ✔
647
                        totLen = parseLength(grabNextMessageLineElement(hProtocolLength));
6 ✔
648
                    }
649
                    else {
650
                        hdrLen = parseLength(replyToOrHdrLen);
3 ✔
651
                        totLen = parseLength(hdrLenOrTotLen);
3 ✔
652
                    }
653

654
                    if(hSubject==null || hSubject.isEmpty() || hSid==null || hSid.isEmpty()) {
10 !
655
                        throw new IllegalStateException("Bad HMSG control line, missing required fields");
×
656
                    }
657

658
                    this.incoming = new IncomingMessageFactory(hSid, hSubject, hReplyTo, hProtocolLineLength);
9 ✔
659
                    this.msgHeaders = new byte[hdrLen];
4 ✔
660
                    this.msgData = new byte[totLen - hdrLen];
6 ✔
661
                    this.mode = Mode.GATHER_HEADERS;
3 ✔
662
                    this.msgHeadersPosition = 0;
3 ✔
663
                    this.msgDataPosition = 0;
3 ✔
664
                    this.msgLinePosition = 0;
3 ✔
665
                    break;
1 ✔
666
                case OP_OK:
667
                    this.connection.processOK();
3 ✔
668
                    readListener.protocol(op, null);
6 ✔
669
                    this.op = UNKNOWN_OP;
3 ✔
670
                    this.mode = Mode.GATHER_OP;
3 ✔
671
                    break;
1 ✔
672
                case OP_ERR:
673
                    String errorText = StandardCharsets.UTF_8.decode(protocolBuffer).toString().replace("'", "");
9 ✔
674
                    this.connection.processError(errorText);
4 ✔
675
                    readListener.protocol(op, errorText);
6 ✔
676
                    this.op = UNKNOWN_OP;
3 ✔
677
                    this.mode = Mode.GATHER_OP;
3 ✔
678
                    break;
1 ✔
679
                case OP_PING:
680
                    readListener.protocol(op, null);
6 ✔
681
                    this.connection.sendPong();
3 ✔
682
                    this.op = UNKNOWN_OP;
3 ✔
683
                    this.mode = Mode.GATHER_OP;
3 ✔
684
                    break;
1 ✔
685
                case OP_PONG:
686
                    readListener.protocol(op, null);
6 ✔
687
                    this.connection.handlePong();
3 ✔
688
                    this.op = UNKNOWN_OP;
3 ✔
689
                    this.mode = Mode.GATHER_OP;
3 ✔
690
                    break;
1 ✔
691
                case OP_INFO:
692
                    String info = StandardCharsets.UTF_8.decode(protocolBuffer).toString();
6 ✔
693
                    this.connection.handleInfo(info);
4 ✔
694
                    readListener.protocol(op, null);
6 ✔
695
                    this.op = UNKNOWN_OP;
3 ✔
696
                    this.mode = Mode.GATHER_OP;
3 ✔
697
                    break;
1 ✔
698
                default:
699
                    throw new IllegalStateException("Unknown protocol operation "+op);
7 ✔
700
            }
701
        }
702
        catch (IllegalStateException | NumberFormatException | NullPointerException ex) {
1 ✔
703
            throw new IOException("Parse Protocol OP_" + op, ex);
8 ✔
704
        }
1 ✔
705
    }
1 ✔
706

707
    //For testing
708
    void fakeReadForTest(byte[] bytes) {
709
        for (int x = 0; x < this.buffer.length; x++) {
9 ✔
710
            this.buffer[x] = 0;
5 ✔
711
        }
712
        System.arraycopy(bytes, 0, this.buffer, 0, bytes.length);
8 ✔
713
        this.bufferPosition = 0;
3 ✔
714
        this.op = UNKNOWN_OP;
3 ✔
715
        this.mode = Mode.GATHER_OP;
3 ✔
716
    }
1 ✔
717

718
    String currentOp() {
719
        return this.op;
3 ✔
720
    }
721
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc