Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 49 additions & 32 deletions src/main/java/io/nats/client/impl/Headers.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
package io.nats.client.impl;

import io.nats.client.support.ByteArrayBuilder;
import org.jspecify.annotations.Nullable;

import java.util.*;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;

import static io.nats.client.support.NatsConstants.*;
import static java.nio.charset.StandardCharsets.US_ASCII;
Expand All @@ -36,8 +38,7 @@ public class Headers {

private final Map<String, List<String>> valuesMap;
private final Map<String, Integer> lengthMap;
private final boolean readOnly;
private byte[] serialized;
private byte @Nullable [] serialized;
private int dataLength;

public Headers() {
Expand All @@ -52,7 +53,7 @@ public Headers(Headers headers, boolean readOnly) {
this(headers, readOnly, null);
}

public Headers(Headers headers, boolean readOnly, String[] keysNotToCopy) {
public Headers(@Nullable Headers headers, boolean readOnly, String @Nullable [] keysNotToCopy) {
Map<String, List<String>> tempValuesMap = new HashMap<>();
Map<String, Integer> tempLengthMap = new HashMap<>();
if (headers != null) {
Expand All @@ -69,7 +70,6 @@ public Headers(Headers headers, boolean readOnly, String[] keysNotToCopy) {
}
}
}
this.readOnly = readOnly;
if (readOnly) {
valuesMap = Collections.unmodifiableMap(tempValuesMap);
lengthMap = Collections.unmodifiableMap(tempLengthMap);
Expand All @@ -92,7 +92,7 @@ public Headers(Headers headers, boolean readOnly, String[] keysNotToCopy) {
* -or- if any value contains invalid characters
*/
public Headers add(String key, String... values) {
if (readOnly) {
if (isReadOnly()) {
throw new UnsupportedOperationException();
}
if (values == null || values.length == 0) {
Expand All @@ -113,7 +113,7 @@ public Headers add(String key, String... values) {
* -or- if any value contains invalid characters
*/
public Headers add(String key, Collection<String> values) {
if (readOnly) {
if (isReadOnly()) {
throw new UnsupportedOperationException();
}
if (values == null || values.isEmpty()) {
Expand Down Expand Up @@ -153,7 +153,7 @@ private Headers _add(String key, Collection<String> values) {
* -or- if any value contains invalid characters
*/
public Headers put(String key, String... values) {
if (readOnly) {
if (isReadOnly()) {
throw new UnsupportedOperationException();
}
if (values == null || values.length == 0) {
Expand All @@ -174,7 +174,7 @@ public Headers put(String key, String... values) {
* -or- if any value contains invalid characters
*/
public Headers put(String key, Collection<String> values) {
if (readOnly) {
if (isReadOnly()) {
throw new UnsupportedOperationException();
}
if (values == null || values.isEmpty()) {
Expand All @@ -191,14 +191,14 @@ public Headers put(String key, Collection<String> values) {
* @return the Headers object
*/
public Headers put(Map<String, List<String>> map) {
if (readOnly) {
if (isReadOnly()) {
throw new UnsupportedOperationException();
}
if (map == null || map.isEmpty()) {
return this;
}
for (String key : map.keySet() ) {
_put(key, map.get(key));
for (Map.Entry<String, List<String>> entry : map.entrySet()) {
_put(entry.getKey(), entry.getValue());
}
return this;
}
Expand Down Expand Up @@ -228,7 +228,7 @@ private Headers _put(String key, Collection<String> values) {
* @param keys the key or keys to remove
*/
public void remove(String... keys) {
if (readOnly) {
if (isReadOnly()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert

throw new UnsupportedOperationException();
}
for (String key : keys) {
Expand All @@ -243,7 +243,7 @@ public void remove(String... keys) {
* @param keys the key or keys to remove
*/
public void remove(Collection<String> keys) {
if (readOnly) {
if (isReadOnly()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert

throw new UnsupportedOperationException();
}
for (String key : keys) {
Expand Down Expand Up @@ -282,7 +282,7 @@ public boolean isEmpty() {
* Removes all the keys The object map will be empty after this call returns.
*/
public void clear() {
if (readOnly) {
if (isReadOnly()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert

throw new UnsupportedOperationException();
}
valuesMap.clear();
Expand Down Expand Up @@ -331,7 +331,7 @@ public Set<String> keySet() {
* @return a read-only set of keys (in lowercase) contained in this map
*/
public Set<String> keySetIgnoreCase() {
HashSet<String> set = new HashSet<>();
HashSet<String> set = new HashSet<>(valuesMap.size()*4/3 + 1);
for (String k : valuesMap.keySet()) {
set.add(k.toLowerCase());
}
Expand All @@ -345,7 +345,7 @@ public Set<String> keySetIgnoreCase() {
* @param key the key whose associated value is to be returned
* @return a read-only list of the values for the case-sensitive key.
*/
public List<String> get(String key) {
public @Nullable List<String> get(String key) {
List<String> values = valuesMap.get(key);
return values == null ? null : Collections.unmodifiableList(values);
}
Expand All @@ -356,7 +356,7 @@ public List<String> get(String key) {
* @param key the key whose associated value is to be returned
* @return the first value for the case-sensitive key.
*/
public String getFirst(String key) {
public @Nullable String getFirst(String key) {
List<String> values = valuesMap.get(key);
return values == null ? null : values.get(0);
}
Expand All @@ -368,7 +368,7 @@ public String getFirst(String key) {
* @param key the key whose associated value is to be returned
* @return the last value for the case-sensitive key.
*/
public String getLast(String key) {
public @Nullable String getLast(String key) {
List<String> values = valuesMap.get(key);
return values == null ? null : values.get(values.size() - 1);
}
Expand All @@ -380,11 +380,11 @@ public String getLast(String key) {
* @param key the key whose associated value is to be returned
* @return a read-only list of the values for the case-insensitive key.
*/
public List<String> getIgnoreCase(String key) {
public @Nullable List<String> getIgnoreCase(String key) {
List<String> values = new ArrayList<>();
for (String k : valuesMap.keySet()) {
if (k.equalsIgnoreCase(key)) {
values.addAll(valuesMap.get(k));
for (Map.Entry<String, List<String>> entry : valuesMap.entrySet()) {
if (entry.getKey().equalsIgnoreCase(key)) {
values.addAll(entry.getValue());
}
}
return values.isEmpty() ? null : Collections.unmodifiableList(values);
Expand All @@ -401,7 +401,8 @@ public List<String> getIgnoreCase(String key) {
* removed during iteration
*/
public void forEach(BiConsumer<String, List<String>> action) {
Collections.unmodifiableMap(valuesMap).forEach(action);
valuesMap.forEach((key, values) ->
action.accept(key, Collections.unmodifiableList(values)));
}

/**
Expand Down Expand Up @@ -460,9 +461,9 @@ public byte[] getSerialized() {
@Deprecated
public ByteArrayBuilder appendSerialized(ByteArrayBuilder bab) {
bab.append(HEADER_VERSION_BYTES_PLUS_CRLF);
for (String key : valuesMap.keySet()) {
for (String value : valuesMap.get(key)) {
bab.append(key);
for (Map.Entry<String, List<String>> entry : valuesMap.entrySet()) {
for (String value : entry.getValue()) {
bab.append(entry.getKey());
bab.append(COLON_BYTES);
bab.append(value);
bab.append(CRLF_BYTES);
Expand Down Expand Up @@ -535,11 +536,12 @@ private void checkKey(String key) {
private void checkValue(String val) {
// Like rfc822 section 3.1.2 (quoted in ADR 4)
// The field-body may be composed of any US-ASCII characters, except CR or LF.
val.chars().forEach(c -> {
for (int i = 0, len = val.length(); i < len; i++) {
int c = val.charAt(i);
if (c > 127 || c == 10 || c == 13) {
throw new IllegalArgumentException(VALUE_INVALID_CHARACTERS + c);
throw new IllegalArgumentException(VALUE_INVALID_CHARACTERS + Integer.toHexString(c));
}
});
}
}

private class Checker {
Expand Down Expand Up @@ -575,19 +577,34 @@ boolean hasValues() {
* @return the read only state
*/
public boolean isReadOnly() {
return readOnly;
return !(valuesMap instanceof HashMap);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!(o instanceof Headers)) return false;
Headers headers = (Headers) o;
return Objects.equals(valuesMap, headers.valuesMap);
}

@Override
public int hashCode() {
return Objects.hash(valuesMap);
return Objects.hashCode(valuesMap);
}

@Override
public String toString() {
return valuesMap.entrySet().stream()
.filter(e -> e.getValue() != null)
.sorted(Map.Entry.comparingByKey())
.map(e -> {
String headerName = e.getKey();
List<String> values = e.getValue();
return headerName +": "+ (values.size() == 1 ? values.get(0)
: String.join(", ", values)
);
})
.collect(Collectors.joining("; "));
}
}
42 changes: 42 additions & 0 deletions src/test/java/io/nats/client/impl/HeadersTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,13 @@ public void testReadOnly() {
assertTrue(headers1.isReadOnly());
assertThrows(UnsupportedOperationException.class, () -> headers1.put(KEY1, VAL2));
assertThrows(UnsupportedOperationException.class, () -> headers1.put(KEY1, VAL2));
assertThrows(UnsupportedOperationException.class, () -> headers1.put(KEY1, VAL1, VAL2));
assertThrows(UnsupportedOperationException.class, () -> headers1.put(KEY1, Arrays.asList(VAL1, VAL2)));
assertThrows(UnsupportedOperationException.class, () -> headers1.remove(KEY1));
assertThrows(UnsupportedOperationException.class, () -> headers1.remove(KEY1,KEY2));
assertThrows(UnsupportedOperationException.class, () -> headers1.remove(Arrays.asList(KEY1,KEY2)));
assertThrows(UnsupportedOperationException.class, () -> headers1.add(KEY1, VAL2));
assertThrows(UnsupportedOperationException.class, () -> headers1.add(KEY1, Arrays.asList(VAL1, VAL2)));
assertThrows(UnsupportedOperationException.class, headers1::clear);
assertEquals(VAL1, headers1.getFirst(KEY1));
}
Expand Down Expand Up @@ -761,6 +767,16 @@ public void testTokenSamePoint() {
@Test
public void testToString() {
assertNotNull(new Status(1, "msg").toString()); // COVERAGE

Headers h = new Headers();
h.add("Test1");
h.add("Test2", "Test2Value");
h.add("Test3", "");
h.add("Test4", "", "", "");
h.add("Test5", "Nice!", "To.", "See?");

assertEquals("Test2: Test2Value; Test3: ; Test4: , , ; Test5: Nice!, To., See?", h.toString()
);
}

@Test
Expand All @@ -782,4 +798,30 @@ public void put_map_works() {
assertTrue(h.get(KEY2).contains(VAL3));
assertEquals(VAL2, h.getFirst(KEY2));
}

@Test
void testForEach() {
Headers h = new Headers();
h.put("test", "a","b","c");
h.forEach((k, v) -> {
assertEquals("test", k);
assertContainsExactly(v, "a", "b", "c");
assertThrows(UnsupportedOperationException.class, ()->v.add("z"));
});
}

/// @see io.nats.client.impl.Headers#checkValue
@Test
void testCheckValue() {
Headers h = new Headers();
h.put("test1", "\u0000 \f\b\t");

assertThrows(IllegalArgumentException.class, ()->h.put("test", "×"));
assertThrows(IllegalArgumentException.class, ()->h.put("test", "\r"));
assertThrows(IllegalArgumentException.class, ()->h.put("test", "\n"));

assertEquals(1, h.size());
assertEquals(1, h.get("test1").size());
assertEquals("\u0000 \f\b\t", h.getFirst("test1"));
}
}