io.nats.client.impl.Headers Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jnats Show documentation
Show all versions of jnats Show documentation
Client library for working with the NATS messaging system.
// Copyright 2020 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at:
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package io.nats.client.impl;
import io.nats.client.support.ByteArrayBuilder;
import java.util.*;
import java.util.function.BiConsumer;
import static io.nats.client.support.NatsConstants.*;
import static java.nio.charset.StandardCharsets.US_ASCII;
/**
* An object that represents a map of keys to a list of values. It does not accept
* null or invalid keys. It ignores null values, accepts empty string as a value
* and rejects invalid values.
* !!!
* THIS CLASS IS NOT THREAD SAFE
*/
public class Headers {
private static final String KEY_CANNOT_BE_EMPTY_OR_NULL = "Header key cannot be null.";
private static final String KEY_INVALID_CHARACTER = "Header key has invalid character: ";
private static final String VALUE_INVALID_CHARACTERS = "Header value has invalid character: ";
private final Map> valuesMap;
private final Map lengthMap;
private final boolean readOnly;
private byte[] serialized;
private int dataLength;
public Headers() {
this(null, false, null);
}
public Headers(Headers headers) {
this(headers, false, null);
}
public Headers(Headers headers, boolean readOnly) {
this(headers, readOnly, null);
}
public Headers(Headers headers, boolean readOnly, String[] keysNotToCopy) {
Map> tempValuesMap = new HashMap<>();
Map tempLengthMap = new HashMap<>();
if (headers != null) {
tempValuesMap.putAll(headers.valuesMap);
tempLengthMap.putAll(headers.lengthMap);
dataLength = headers.dataLength;
if (keysNotToCopy != null) {
for (String key : keysNotToCopy) {
if (key != null) {
if (tempValuesMap.remove(key) != null) {
dataLength -= tempLengthMap.remove(key);
}
}
}
}
}
this.readOnly = readOnly;
if (readOnly) {
valuesMap = Collections.unmodifiableMap(tempValuesMap);
lengthMap = Collections.unmodifiableMap(tempLengthMap);
}
else {
valuesMap = tempValuesMap;
lengthMap = tempLengthMap;
}
}
/**
* If the key is present add the values to the list of values for the key.
* If the key is not present, sets the specified values for the key.
* null values are ignored. If all values are null, the key is not added or updated.
*
* @param key the key
* @param values the values
* @return the Headers object
* @throws IllegalArgumentException if the key is null or empty or contains invalid characters
* -or- if any value contains invalid characters
*/
public Headers add(String key, String... values) {
if (readOnly) {
throw new UnsupportedOperationException();
}
if (values == null || values.length == 0) {
return this;
}
return _add(key, Arrays.asList(values));
}
/**
* If the key is present add the values to the list of values for the key.
* If the key is not present, sets the specified values for the key.
* null values are ignored. If all values are null, the key is not added or updated.
*
* @param key the entry key
* @param values a list of values to the entry
* @return the Header object
* @throws IllegalArgumentException if the key is null or empty or contains invalid characters
* -or- if any value contains invalid characters
*/
public Headers add(String key, Collection values) {
if (readOnly) {
throw new UnsupportedOperationException();
}
if (values == null || values.isEmpty()) {
return this;
}
return _add(key, values);
}
// the add delegate
private Headers _add(String key, Collection values) {
if (values != null) {
Checker checked = new Checker(key, values);
if (checked.hasValues()) {
// get values by key or compute empty if absent
// update the data length with the additional len
// update the lengthMap for the key to the old length plus the new length
List currentSet = valuesMap.computeIfAbsent(key, k -> new ArrayList<>());
currentSet.addAll(checked.list);
dataLength += checked.len;
int oldLen = lengthMap.getOrDefault(key, 0);
lengthMap.put(key, oldLen + checked.len);
serialized = null; // since the data changed, clear this so it's rebuilt
}
}
return this;
}
/**
* Associates the specified values with the key. If the key was already present
* any existing values are removed and replaced with the new list.
* null values are ignored. If all values are null, the put is ignored
*
* @param key the key
* @param values the values
* @return the Headers object
* @throws IllegalArgumentException if the key is null or empty or contains invalid characters
* -or- if any value contains invalid characters
*/
public Headers put(String key, String... values) {
if (readOnly) {
throw new UnsupportedOperationException();
}
if (values == null || values.length == 0) {
return this;
}
return _put(key, Arrays.asList(values));
}
/**
* Associates the specified values with the key. If the key was already present
* any existing values are removed and replaced with the new list.
* null values are ignored. If all values are null, the put is ignored
*
* @param key the key
* @param values the values
* @return the Headers object
* @throws IllegalArgumentException if the key is null or empty or contains invalid characters
* -or- if any value contains invalid characters
*/
public Headers put(String key, Collection values) {
if (readOnly) {
throw new UnsupportedOperationException();
}
if (values == null || values.isEmpty()) {
return this;
}
return _put(key, values);
}
/**
* Associates all specified values with their key. If the key was already present
* any existing values are removed and replaced with the new list.
* null values are ignored. If all values are null, the put is ignored
* @param map the map
* @return the Headers object
*/
public Headers put(Map> map) {
if (readOnly) {
throw new UnsupportedOperationException();
}
if (map == null || map.isEmpty()) {
return this;
}
for (String key : map.keySet() ) {
_put(key, map.get(key));
}
return this;
}
// the put delegate
private Headers _put(String key, Collection values) {
if (key == null || key.isEmpty()) {
throw new IllegalArgumentException("Key cannot be null or empty.");
}
if (values != null) {
Checker checked = new Checker(key, values);
if (checked.hasValues()) {
// update the data length removing the old length adding the new length
// put for the key
dataLength = dataLength - lengthMap.getOrDefault(key, 0) + checked.len;
valuesMap.put(key, checked.list);
lengthMap.put(key, checked.len);
serialized = null; // since the data changed, clear this so it's rebuilt
}
}
return this;
}
/**
* Removes each key and its values if the key was present
*
* @param keys the key or keys to remove
*/
public void remove(String... keys) {
if (readOnly) {
throw new UnsupportedOperationException();
}
for (String key : keys) {
_remove(key);
}
serialized = null; // since the data changed, clear this so it's rebuilt
}
/**
* Removes each key and its values if the key was present
*
* @param keys the key or keys to remove
*/
public void remove(Collection keys) {
if (readOnly) {
throw new UnsupportedOperationException();
}
for (String key : keys) {
_remove(key);
}
serialized = null; // since the data changed, clear this so it's rebuilt
}
// the remove delegate
private void _remove(String key) {
// if the values had a key, then the data length had a length
if (valuesMap.remove(key) != null) {
dataLength -= lengthMap.remove(key);
}
}
/**
* Returns the number of keys (case-sensitive) in the header.
*
* @return the number of header entries
*/
public int size() {
return valuesMap.size();
}
/**
* Returns ture if map contains no keys.
*
* @return true if there are no headers
*/
public boolean isEmpty() {
return valuesMap.isEmpty();
}
/**
* Removes all the keys The object map will be empty after this call returns.
*/
public void clear() {
if (readOnly) {
throw new UnsupportedOperationException();
}
valuesMap.clear();
lengthMap.clear();
dataLength = 0;
serialized = null;
}
/**
* Returns true if key (case-sensitive) is present (has values)
*
* @param key key whose presence is to be tested
* @return true if the key (case-sensitive) is present (has values)
*/
public boolean containsKey(String key) {
return valuesMap.containsKey(key);
}
/**
* Returns true if key (case-insensitive) is present (has values)
*
* @param key exact key whose presence is to be tested
* @return true if the key (case-insensitive) is present (has values)
*/
public boolean containsKeyIgnoreCase(String key) {
for (String k : valuesMap.keySet()) {
if (k.equalsIgnoreCase(key)) {
return true;
}
}
return false;
}
/**
* Returns a {@link Set} view of the keys (case-sensitive) contained in the object.
*
* @return a read-only set the keys contained in this map
*/
public Set keySet() {
return Collections.unmodifiableSet(valuesMap.keySet());
}
/**
* Returns a {@link Set} view of the keys (case-insensitive) contained in the object.
*
* @return a read-only set of keys (in lowercase) contained in this map
*/
public Set keySetIgnoreCase() {
HashSet set = new HashSet<>();
for (String k : valuesMap.keySet()) {
set.add(k.toLowerCase());
}
return Collections.unmodifiableSet(set);
}
/**
* Returns a {@link List} view of the values for the specific (case-sensitive) key.
* Will be {@code null} if the key is not found.
*
* @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 get(String key) {
List values = valuesMap.get(key);
return values == null ? null : Collections.unmodifiableList(values);
}
/**
* Returns the first value for the specific (case-sensitive) key.
* Will be {@code null} if the key is not found.
* @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) {
List values = valuesMap.get(key);
return values == null ? null : values.get(0);
}
/**
* Returns the last value for the specific (case-sensitive) key.
* Will be {@code null} if the key is not found.
*
* @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) {
List values = valuesMap.get(key);
return values == null ? null : values.get(values.size() - 1);
}
/**
* Returns a {@link List} view of the values for the specific (case-insensitive) key.
* Will be {@code null} if the key is not found.
*
* @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 getIgnoreCase(String key) {
List values = new ArrayList<>();
for (String k : valuesMap.keySet()) {
if (k.equalsIgnoreCase(key)) {
values.addAll(valuesMap.get(k));
}
}
return values.isEmpty() ? null : Collections.unmodifiableList(values);
}
/**
* Performs the given action for each header entry (case-sensitive keys) until all entries
* have been processed or the action throws an exception.
* Any attempt to modify the values will throw an exception.
*
* @param action The action to be performed for each entry
* @throws NullPointerException if the specified action is null
* @throws ConcurrentModificationException if an entry is found to be
* removed during iteration
*/
public void forEach(BiConsumer> action) {
Collections.unmodifiableMap(valuesMap).forEach(action);
}
/**
* Returns a {@link Set} read only view of the mappings contained in the header (case-sensitive keys).
* The set is not modifiable and any attempt to modify will throw an exception.
*
* @return a set view of the mappings contained in this map
*/
public Set>> entrySet() {
return Collections.unmodifiableSet(valuesMap.entrySet());
}
/**
* Returns if the headers are dirty, which means the serialization
* has not been done so also don't know the byte length
*
* @return true if dirty
*/
public boolean isDirty() {
return serialized == null;
}
/**
* Returns the number of bytes that will be in the serialized version.
*
* @return the number of bytes
*/
public int serializedLength() {
return dataLength + NON_DATA_BYTES;
}
private static final int HVCRLF_BYTES = HEADER_VERSION_BYTES_PLUS_CRLF.length;
private static final int NON_DATA_BYTES = HVCRLF_BYTES + 2;
/**
* Returns the serialized bytes.
*
* @return the bytes
*/
public byte[] getSerialized() {
if (serialized == null) {
serialized = new byte[serializedLength()];
serializeToArray(0, serialized);
}
return serialized;
}
/**
* @deprecated
* Used for unit testing.
* Appends the serialized bytes to the builder.
*
* @param bab the ByteArrayBuilder to append
* @return the builder
*/
@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);
bab.append(COLON_BYTES);
bab.append(value);
bab.append(CRLF_BYTES);
}
}
bab.append(CRLF_BYTES);
return bab;
}
/**
* Write the header to the byte array. Assumes that the caller has
* already validated that the destination array is large enough by using getSerialized()
* @param destPosition the position index in destination byte array to start
* @param dest the byte array to write to
* @return the length of the header
*/
public int serializeToArray(int destPosition, byte[] dest) {
System.arraycopy(HEADER_VERSION_BYTES_PLUS_CRLF, 0, dest, destPosition, HVCRLF_BYTES);
destPosition += HVCRLF_BYTES;
for (Map.Entry> entry : valuesMap.entrySet()) {
List values = entry.getValue();
for (String value : values) {
byte[] bytes = entry.getKey().getBytes(US_ASCII);
System.arraycopy(bytes, 0, dest, destPosition, bytes.length);
destPosition += bytes.length;
dest[destPosition++] = COLON;
bytes = value.getBytes(US_ASCII);
System.arraycopy(bytes, 0, dest, destPosition, bytes.length);
destPosition += bytes.length;
dest[destPosition++] = CR;
dest[destPosition++] = LF;
}
}
dest[destPosition++] = CR;
dest[destPosition] = LF;
return serializedLength();
}
/**
* Check the key to ensure it matches the specification for keys.
*
* @throws IllegalArgumentException if the key is null, empty or contains
* an invalid character
*/
private void checkKey(String key) {
// key cannot be null or empty and contain only printable characters except colon
if (key == null || key.isEmpty()) {
throw new IllegalArgumentException(KEY_CANNOT_BE_EMPTY_OR_NULL);
}
int len = key.length();
for (int idx = 0; idx < len; idx++) {
char c = key.charAt(idx);
if (c < 33 || c > 126 || c == ':') {
throw new IllegalArgumentException(KEY_INVALID_CHARACTER + "'" + c + "'");
}
}
}
/**
* Check a non-null value if it matches the specification for values.
*
* @throws IllegalArgumentException if the value contains an invalid character
*/
private void checkValue(String val) {
// Generally more permissive than HTTP. Allow only printable
// characters and include tab (0x9) to cover what's allowed
// in quoted strings and comments.
val.chars().forEach(c -> {
if ((c < 32 && c != 9) || c > 126) {
throw new IllegalArgumentException(VALUE_INVALID_CHARACTERS + c);
}
});
}
private class Checker {
List list = new ArrayList<>();
int len = 0;
Checker(String key, Collection values) {
checkKey(key);
if (!values.isEmpty()) {
for (String val : values) {
if (val != null) {
if (val.isEmpty()) {
list.add(val);
len += key.length() + 3; // for colon, cr, lf
}
else {
checkValue(val);
list.add(val);
len += key.length() + val.length() + 3; // for colon, cr, lf
}
}
}
}
}
boolean hasValues() {
return !list.isEmpty();
}
}
/**
* Whether the entire Headers is read only
* @return the read only state
*/
public boolean isReadOnly() {
return readOnly;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Headers headers = (Headers) o;
return Objects.equals(valuesMap, headers.valuesMap);
}
@Override
public int hashCode() {
return Objects.hash(valuesMap);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy