1- // Copyright 2020 The NATS Authors
1+ // Copyright 2020-2025 The NATS Authors
22// Licensed under the Apache License, Version 2.0 (the "License");
33// you may not use this file except in compliance with the License.
44// You may obtain a copy of the License at:
1414package io .nats .client .impl ;
1515
1616import io .nats .client .support .ByteArrayBuilder ;
17+ import org .jspecify .annotations .Nullable ;
1718
19+ import java .nio .charset .StandardCharsets ;
1820import java .util .*;
1921import java .util .function .BiConsumer ;
20-
2122import static io .nats .client .support .NatsConstants .*;
22- import static java .nio .charset .StandardCharsets .US_ASCII ;
2323
2424/**
2525 * An object that represents a map of keys to a list of values. It does not accept
3131public class Headers {
3232
3333 private static final String KEY_CANNOT_BE_EMPTY_OR_NULL = "Header key cannot be null." ;
34- private static final String KEY_INVALID_CHARACTER = "Header key has invalid character: " ;
35- private static final String VALUE_INVALID_CHARACTERS = "Header value has invalid character: " ;
34+ private static final String KEY_INVALID_CHARACTER = "Header key has invalid character: 0x " ;
35+ private static final String VALUE_INVALID_CHARACTERS = "Header value has invalid character: 0x " ;
3636
3737 private final Map <String , List <String >> valuesMap ;
3838 private final Map <String , Integer > lengthMap ;
3939 private final boolean readOnly ;
40- private byte [] serialized ;
40+ private byte @ Nullable [] serialized ;
4141 private int dataLength ;
4242
4343 public Headers () {
@@ -52,7 +52,7 @@ public Headers(Headers headers, boolean readOnly) {
5252 this (headers , readOnly , null );
5353 }
5454
55- public Headers (Headers headers , boolean readOnly , String [] keysNotToCopy ) {
55+ public Headers (@ Nullable Headers headers , boolean readOnly , String @ Nullable [] keysNotToCopy ) {
5656 Map <String , List <String >> tempValuesMap = new HashMap <>();
5757 Map <String , Integer > tempLengthMap = new HashMap <>();
5858 if (headers != null ) {
@@ -197,8 +197,8 @@ public Headers put(Map<String, List<String>> map) {
197197 if (map == null || map .isEmpty ()) {
198198 return this ;
199199 }
200- for (String key : map .keySet () ) {
201- _put (key , map . get ( key ));
200+ for (Map . Entry < String , List < String >> entry : map .entrySet () ) {
201+ _put (entry . getKey (), entry . getValue ( ));
202202 }
203203 return this ;
204204 }
@@ -331,7 +331,7 @@ public Set<String> keySet() {
331331 * @return a read-only set of keys (in lowercase) contained in this map
332332 */
333333 public Set <String > keySetIgnoreCase () {
334- HashSet <String > set = new HashSet <>();
334+ HashSet <String > set = new HashSet <>();// no capacity is OK for small maps
335335 for (String k : valuesMap .keySet ()) {
336336 set .add (k .toLowerCase ());
337337 }
@@ -345,7 +345,7 @@ public Set<String> keySetIgnoreCase() {
345345 * @param key the key whose associated value is to be returned
346346 * @return a read-only list of the values for the case-sensitive key.
347347 */
348- public List <String > get (String key ) {
348+ public @ Nullable List <String > get (String key ) {
349349 List <String > values = valuesMap .get (key );
350350 return values == null ? null : Collections .unmodifiableList (values );
351351 }
@@ -356,7 +356,7 @@ public List<String> get(String key) {
356356 * @param key the key whose associated value is to be returned
357357 * @return the first value for the case-sensitive key.
358358 */
359- public String getFirst (String key ) {
359+ public @ Nullable String getFirst (String key ) {
360360 List <String > values = valuesMap .get (key );
361361 return values == null ? null : values .get (0 );
362362 }
@@ -368,7 +368,7 @@ public String getFirst(String key) {
368368 * @param key the key whose associated value is to be returned
369369 * @return the last value for the case-sensitive key.
370370 */
371- public String getLast (String key ) {
371+ public @ Nullable String getLast (String key ) {
372372 List <String > values = valuesMap .get (key );
373373 return values == null ? null : values .get (values .size () - 1 );
374374 }
@@ -380,11 +380,11 @@ public String getLast(String key) {
380380 * @param key the key whose associated value is to be returned
381381 * @return a read-only list of the values for the case-insensitive key.
382382 */
383- public List <String > getIgnoreCase (String key ) {
383+ public @ Nullable List <String > getIgnoreCase (String key ) {
384384 List <String > values = new ArrayList <>();
385- for (String k : valuesMap .keySet ()) {
386- if (k .equalsIgnoreCase (key )) {
387- values .addAll (valuesMap . get ( k ));
385+ for (Map . Entry < String , List < String >> entry : valuesMap .entrySet ()) {
386+ if (entry . getKey () .equalsIgnoreCase (key )) {
387+ values .addAll (entry . getValue ( ));
388388 }
389389 }
390390 return values .isEmpty () ? null : Collections .unmodifiableList (values );
@@ -401,7 +401,9 @@ public List<String> getIgnoreCase(String key) {
401401 * removed during iteration
402402 */
403403 public void forEach (BiConsumer <String , List <String >> action ) {
404- Collections .unmodifiableMap (valuesMap ).forEach (action );
404+ for (Map .Entry <String , List <String >> entry : valuesMap .entrySet ()) {
405+ action .accept (entry .getKey (), Collections .unmodifiableList (entry .getValue ()));
406+ }
405407 }
406408
407409 /**
@@ -460,9 +462,9 @@ public byte[] getSerialized() {
460462 @ Deprecated
461463 public ByteArrayBuilder appendSerialized (ByteArrayBuilder bab ) {
462464 bab .append (HEADER_VERSION_BYTES_PLUS_CRLF );
463- for (String key : valuesMap .keySet ()) {
464- for (String value : valuesMap . get ( key )) {
465- bab .append (key );
465+ for (Map . Entry < String , List < String >> entry : valuesMap .entrySet ()) {
466+ for (String value : entry . getValue ( )) {
467+ bab .append (entry . getKey () );
466468 bab .append (COLON_BYTES );
467469 bab .append (value );
468470 bab .append (CRLF_BYTES );
@@ -474,27 +476,28 @@ public ByteArrayBuilder appendSerialized(ByteArrayBuilder bab) {
474476
475477 /**
476478 * Write the header to the byte array. Assumes that the caller has
477- * already validated that the destination array is large enough by using getSerialized()
479+ * already validated that the destination array is large enough by using {@link #getSerialized()}.
480+ * <p>/Deprecated {@link String#getBytes(int, int, byte[], int)} is used, because it still exists in JDK 25
481+ * and is 10–30 times faster than {@code getBytes(ISO_8859_1/US_ASCII)}/
478482 * @param destPosition the position index in destination byte array to start
479483 * @param dest the byte array to write to
480484 * @return the length of the header
481485 */
486+ @ SuppressWarnings ("deprecation" )
482487 public int serializeToArray (int destPosition , byte [] dest ) {
483488 System .arraycopy (HEADER_VERSION_BYTES_PLUS_CRLF , 0 , dest , destPosition , HVCRLF_BYTES );
484489 destPosition += HVCRLF_BYTES ;
485490
486491 for (Map .Entry <String , List <String >> entry : valuesMap .entrySet ()) {
487- List <String > values = entry .getValue ();
488- for (String value : values ) {
489- byte [] bytes = entry .getKey ().getBytes (US_ASCII );
490- System .arraycopy (bytes , 0 , dest , destPosition , bytes .length );
491- destPosition += bytes .length ;
492+ String key = entry .getKey ();
493+ for (String value : entry .getValue ()) {
494+ key .getBytes (0 , key .length (), dest , destPosition );// key has only US_ASCII
495+ destPosition += key .length ();
492496
493497 dest [destPosition ++] = COLON ;
494498
495- bytes = value .getBytes (US_ASCII );
496- System .arraycopy (bytes , 0 , dest , destPosition , bytes .length );
497- destPosition += bytes .length ;
499+ value .getBytes (0 , value .length (), dest , destPosition );
500+ destPosition += value .length ();
498501
499502 dest [destPosition ++] = CR ;
500503 dest [destPosition ++] = LF ;
@@ -503,6 +506,7 @@ public int serializeToArray(int destPosition, byte[] dest) {
503506 dest [destPosition ++] = CR ;
504507 dest [destPosition ] = LF ;
505508
509+ //to do update serialized and/or dataLength?
506510 return serializedLength ();
507511 }
508512
@@ -512,7 +516,7 @@ public int serializeToArray(int destPosition, byte[] dest) {
512516 * @throws IllegalArgumentException if the key is null, empty or contains
513517 * an invalid character
514518 */
515- private void checkKey (String key ) {
519+ static void checkKey (String key ) {
516520 // key cannot be null or empty and contain only printable characters except colon
517521 if (key == null || key .isEmpty ()) {
518522 throw new IllegalArgumentException (KEY_CANNOT_BE_EMPTY_OR_NULL );
@@ -522,7 +526,7 @@ private void checkKey(String key) {
522526 for (int idx = 0 ; idx < len ; idx ++) {
523527 char c = key .charAt (idx );
524528 if (c < 33 || c > 126 || c == ':' ) {
525- throw new IllegalArgumentException (KEY_INVALID_CHARACTER + "'" + c + "'" );
529+ throw new IllegalArgumentException (KEY_INVALID_CHARACTER + Integer . toHexString ( c ) );
526530 }
527531 }
528532 }
@@ -532,17 +536,18 @@ private void checkKey(String key) {
532536 *
533537 * @throws IllegalArgumentException if the value contains an invalid character
534538 */
535- private void checkValue (String val ) {
539+ static void checkValue (String val ) {
536540 // Like rfc822 section 3.1.2 (quoted in ADR 4)
537541 // The field-body may be composed of any US-ASCII characters, except CR or LF.
538- val .chars ().forEach (c -> {
542+ for (int i = 0 , len = val .length (); i < len ; i ++) {
543+ int c = val .charAt (i );
539544 if (c > 127 || c == 10 || c == 13 ) {
540- throw new IllegalArgumentException (VALUE_INVALID_CHARACTERS + c );
545+ throw new IllegalArgumentException (VALUE_INVALID_CHARACTERS + Integer . toHexString ( c ) );
541546 }
542- });
547+ }
543548 }
544549
545- private class Checker {
550+ private static final class Checker {
546551 List <String > list = new ArrayList <>();
547552 int len = 0 ;
548553
@@ -581,13 +586,29 @@ public boolean isReadOnly() {
581586 @ Override
582587 public boolean equals (Object o ) {
583588 if (this == o ) return true ;
584- if (o == null || getClass () != o . getClass ( )) return false ;
589+ if (!( o instanceof Headers )) return false ;
585590 Headers headers = (Headers ) o ;
586591 return Objects .equals (valuesMap , headers .valuesMap );
587592 }
588593
589594 @ Override
590595 public int hashCode () {
591- return Objects .hash (valuesMap );
596+ return Objects .hashCode (valuesMap );
597+ }
598+
599+ @ Override
600+ public String toString () {
601+ byte [] b = getSerialized ();
602+ int len = b .length ;
603+ if (len <= HVCRLF_BYTES + 2 ){
604+ return "" ;// empty map
605+ }
606+ for (int i = 0 ; i < len ; i ++) {
607+ switch (b [i ]) {
608+ case CR : b [i ] = ';' ; break ;
609+ case LF : b [i ] = ' ' ; break ;
610+ }
611+ }
612+ return new String (b , HVCRLF_BYTES , len - HVCRLF_BYTES - 3 , StandardCharsets .ISO_8859_1 );// b has only US_ASCII, ISO_8859_1 is 3x faster
592613 }
593614}
0 commit comments