io.netty5.handler.codec.http.headers.MultiMap Maven / Gradle / Ivy
/*
* Copyright 2022 The Netty Project
*
* The Netty Project licenses this file to you 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:
*
* https://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.
*/
/*
* Copyright © 2018 Apple Inc. and the ServiceTalk project 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
*
* https://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.netty5.handler.codec.http.headers;
import io.netty5.util.internal.UnstableApi;
import org.jetbrains.annotations.ApiStatus.Internal;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.function.BiPredicate;
import static io.netty5.handler.codec.http.headers.HeaderUtils.HASH_CODE_SEED;
import static io.netty5.util.internal.MathUtil.findNextPositivePowerOfTwo;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.util.Collections.emptyIterator;
import static java.util.Collections.emptySet;
import static java.util.Objects.requireNonNull;
/**
* A {@link Map} like implementation which supports multiple values for a single key.
* Implementation Details (subject to change)
* This class is designed to store "headers" which are commonly used in protocols to represent meta data.
* Common protocols typically have the following properties:
*
* - Fast overall iteration - encoding typical requires iteration
* - Fast lookup for single value - a single value associative array is a common usage pattern
* - Multi value lookup - some use cases require multi value associative array like storage
* - Consistent iteration for a given key - the iteration order for a multi value lookup {@link #getValues(Object)}
* should reflect the insertion order for each key
* - Avoid copy/resize operations - headers can be created frequently and cause GC pressure, so we should try to
* minimize intermediate GC.
*
* These requirements make using a Map<K, List<V>> structure prohibitive due to extra allocation and resize
* operations.
*
* @param The type of key.
* @param The type of value.
*/
@UnstableApi
@Internal
public abstract class MultiMap {
final BucketHead[] entries;
@Nullable
BucketHead lastBucketHead;
private final byte hashMask;
private int size;
@SuppressWarnings("unchecked")
MultiMap(final int arraySizeHint) {
// Enforce a bound of [2, 128] because hashMask is a byte. The max possible value of hashMask is one less
// than the length of this array, and we want the mask to be > 0.
entries = (BucketHead[]) new BucketHead[findNextPositivePowerOfTwo(max(2, min(arraySizeHint, 128)))];
hashMask = (byte) (entries.length - 1);
}
/**
* Generate a hash code for {@code key} used as an index in this {@link MultiMap}.
*
* @param key The key to create the hash code for.
* @return a hash code for {@code key} used as an index in this {@link MultiMap}.
*/
@Internal
protected abstract int hashCode(K key);
/**
* Compare {@code key1} and {@code key2} for equality.
*
* @param key1 The first key.
* @param key2 The second key.
* @return {@code true} if {@code key1} and {@code key2} are equal.
*/
@Internal
protected abstract boolean equals(K key1, K key2);
/**
* Determine if the {@link #hashCode(Object)} and {@link #equals(Object, Object)} strategy are compatible with
* {@code multiMap}.
*
* @param multiMap the {@link MultiMap} to compare.
* @return {@code true} if the {@link #hashCode(Object)} and {@link #equals(Object, Object)} strategy are compatible
* with {@code multiMap}.
*/
@Internal
protected abstract boolean isKeyEqualityCompatible(MultiMap extends K, ? extends V> multiMap);
/**
* Validate the key before inserting it into this {@link MultiMap}.
*
* @param key The key which will be inserted.
* @param forAdd {@code true} if this validation is for adding to the headers, or {@code false} if this is for
* setting (overwriting) the given header.
*/
@Internal
protected abstract K validateKey(K key, boolean forAdd);
/**
* Validate the value before inserting it into this {@link MultiMap}.
*
* @param key The key for which the value is being inserted, for reference.
* @param value The value which will be inserted.
*/
@Internal
protected abstract V validateValue(K key, V value);
/**
* Generate a hash code for {@code value} using for equality comparisons and {@link #hashCode(Object)}.
*
* @param value the value to generate a hash code for.
* @return a hash code for {@code value} using during equality comparisons and {@link #hashCode(Object)}.
*/
@Internal
protected abstract int hashCodeForValue(V value);
/**
* Compare {@code value1} and {@code value2} for equality.
*
* @param value1 The first value.
* @param value2 The second value.
* @return {@code true} if {@code value1} and {@code value2} are equal.
*/
@Internal
protected abstract boolean equalsForValue(V value1, V value2);
/**
* Create a new {@link MultiMapEntry} to represent an entry in this {@link MultiMap}.
*
* @param key The key for the {@link MultiMapEntry}.
* @param value The value for the {@link MultiMapEntry}.
* @param keyHash The hash code for {@code key}.
* @return a new {@link MultiMapEntry} to represent an entry in this {@link MultiMap}.
*/
private MultiMapEntry newEntry(K key, V value, int keyHash) {
return new MultiMapEntry<>(key, value, keyHash);
}
final Set getKeys() {
if (isEmpty()) {
return emptySet();
}
// Overall iteration order does not need to be preserved.
final Set names = new HashSet<>((int) (size() / .75), .75f);
BucketHead bucketHead = lastBucketHead;
while (bucketHead != null) {
MultiMapEntry e = bucketHead.entry;
assert e != null;
do {
names.add(e.getKey());
e = e.bucketNext;
} while (e != null);
bucketHead = bucketHead.prevBucketHead;
}
return names;
}
@Internal
public final int size() {
return size;
}
@Internal
public final boolean isEmpty() {
return lastBucketHead == null;
}
@Nullable
final V getValue(final K key) {
final int nameHash = hashCode(key);
final int i = index(nameHash);
final BucketHead bucketHead = entries[i];
if (bucketHead != null) {
MultiMapEntry e = bucketHead.entry;
assert e != null;
do {
if (e.keyHash == nameHash && equals(key, e.getKey())) {
return e.value;
}
e = e.bucketNext;
} while (e != null);
}
return null;
}
final Iterator getValues(final K key) {
final int keyHash = hashCode(key);
final BucketHead bucketHead = entries[index(keyHash)];
if (bucketHead == null) {
return emptyIterator();
}
MultiMapEntry e = bucketHead.entry;
assert e != null;
do {
if (e.keyHash == keyHash && equals(key, e.getKey())) {
return new ValuesByNameIterator(keyHash, key, e);
}
e = e.bucketNext;
} while (e != null);
return emptyIterator();
}
@Internal
public boolean contains(final K key, final V value) {
return contains(key, value, this::equalsForValue);
}
final boolean contains(final K key, final V value, final BiPredicate valueCompare) {
final int keyHash = hashCode(key);
final int bucketIndex = index(keyHash);
final BucketHead bucketHead = entries[bucketIndex];
if (bucketHead != null) {
MultiMapEntry e = bucketHead.entry;
assert e != null;
do {
if (e.keyHash == keyHash && equals(key, e.getKey()) && valueCompare.test(value, e.value)) {
return true;
}
e = e.bucketNext;
} while (e != null);
}
return false;
}
final void put(final K key, final V value) {
final int keyHash = hashCode(validateKey(key, true));
final int bucketIndex = index(keyHash);
putEntry(keyHash, bucketIndex, key, validateValue(key, value));
}
final void putAll(final K key, final Iterable extends V> values) {
final int keyHash = hashCode(validateKey(key, true));
final int bucketIndex = index(keyHash);
BucketHead bucketHead = entries[bucketIndex];
if (bucketHead != null) {
for (final V v : values) {
putEntry(bucketHead, keyHash, bucketIndex, key, validateValue(key, v));
}
} else {
final Iterator extends V> valueItr = values.iterator();
if (valueItr.hasNext()) {
bucketHead = putEntry(keyHash, bucketIndex, key, validateValue(key, valueItr.next()));
while (valueItr.hasNext()) {
putEntry(bucketHead, keyHash, bucketIndex, key, validateValue(key, valueItr.next()));
}
}
}
}
@SafeVarargs
final void putAll(final K key, final V... values) {
final int keyHash = hashCode(validateKey(key, true));
final int bucketIndex = index(keyHash);
BucketHead bucketHead = entries[bucketIndex];
if (bucketHead != null) {
for (final V v : values) {
putEntry(bucketHead, keyHash, bucketIndex, key, validateValue(key, v));
}
} else if (values.length != 0) {
bucketHead = putEntry(keyHash, bucketIndex, key, validateValue(key, values[0]));
for (int i = 1; i < values.length; ++i) {
putEntry(bucketHead, keyHash, bucketIndex, key, validateValue(key, values[i]));
}
}
}
final void putAll(final MultiMap extends K, ? extends V> multiMap) {
putAll0(multiMap);
}
final void putExclusive(final K key, final V value) {
final int keyHash = hashCode(validateKey(key, false));
final int bucketIndex = index(keyHash);
removeAll(key, keyHash, bucketIndex);
putEntry(keyHash, bucketIndex, key, validateValue(key, value));
}
final void putExclusive(final K key, final Iterable extends V> values) {
final int keyHash = hashCode(validateKey(key, false));
final int bucketIndex = index(keyHash);
removeAll(key, keyHash, bucketIndex);
final Iterator extends V> valueItr = values.iterator();
if (valueItr.hasNext()) {
BucketHead bucketHead = entries[bucketIndex];
if (bucketHead == null) {
bucketHead = putEntry(keyHash, bucketIndex, key, validateValue(key, valueItr.next()));
if (!valueItr.hasNext()) {
return;
}
}
do {
putEntry(bucketHead, keyHash, bucketIndex, key, validateValue(key, valueItr.next()));
} while (valueItr.hasNext());
}
}
@SafeVarargs
final void putExclusive(final K key, final V... values) {
final int keyHash = hashCode(validateKey(key, false));
final int bucketIndex = index(keyHash);
removeAll(key, keyHash, bucketIndex);
if (values.length != 0) {
BucketHead bucketHead = entries[bucketIndex];
int i = 0;
if (bucketHead == null) {
bucketHead = putEntry(keyHash, bucketIndex, key, validateValue(key, values[0]));
i = 1;
}
for (; i < values.length; ++i) {
putEntry(bucketHead, keyHash, bucketIndex, key, validateValue(key, values[i]));
}
}
}
final void clearAll() {
Arrays.fill(entries, null);
lastBucketHead = null;
size = 0;
}
final boolean removeAll(final K key) {
final int keyHash = hashCode(key);
final int sizeBefore = size;
removeAll(key, keyHash, index(keyHash));
return sizeBefore != size;
}
private void removeAll(final K key, final int keyHash, final int bucketIndex) {
final BucketHead bucketHead = entries[bucketIndex];
if (bucketHead == null) {
return;
}
MultiMapEntry e = bucketHead.entry;
assert e != null;
do {
if (e.keyHash == keyHash && equals(key, e.getKey())) {
final MultiMapEntry tmpEntry = e;
e = e.bucketNext;
removeEntry(bucketHead, tmpEntry, bucketIndex);
} else {
e = e.bucketNext;
}
} while (e != null);
}
@Nullable
final V removeAllAndGetFirst(final K key) {
final int keyHash = hashCode(key);
final int bucketIndex = index(keyHash);
final BucketHead bucketHead = entries[bucketIndex];
if (bucketHead == null) {
return null;
}
V value = null;
MultiMapEntry e = bucketHead.entry;
assert e != null;
do {
if (e.keyHash == keyHash && equals(key, e.getKey())) {
if (value == null) {
value = e.getValue();
}
final MultiMapEntry tmpEntry = e;
e = e.bucketNext;
removeEntry(bucketHead, tmpEntry, bucketIndex);
} else {
e = e.bucketNext;
}
} while (e != null);
return value;
}
final Iterator> entryIterator() {
return lastBucketHead == null ? emptyIterator() : new FullEntryIterator(lastBucketHead);
}
final Iterator valueIterator() {
return lastBucketHead == null ? emptyIterator() : new ValueEntryIterator(lastBucketHead);
}
final int index(final int hash) {
return hash & hashMask;
}
final void removeEntry(@NotNull final BucketHead bucketHead,
@NotNull final MultiMapEntry entryToRemove,
final int bucketIndex) {
// Check to see if the entry to remove is the bucketHead entry.
if (bucketHead.entry == entryToRemove) {
if (bucketHead.entry.bucketNext == null) {
entries[bucketIndex] = null;
if (lastBucketHead == bucketHead) {
// bucketHead is either the last bucket or the only bucket.
if (lastBucketHead.prevBucketHead != null) {
lastBucketHead.prevBucketHead.nextBucketHead = null;
}
lastBucketHead = lastBucketHead.prevBucketHead;
} else {
// bucketHead is either a middle bucket or the first bucket.
assert lastBucketHead != null;
if (bucketHead.prevBucketHead != null) {
bucketHead.prevBucketHead.nextBucketHead = bucketHead.nextBucketHead;
}
assert bucketHead.nextBucketHead != null;
bucketHead.nextBucketHead.prevBucketHead = bucketHead.prevBucketHead;
}
} else {
// The next entry will now be the bucket head. We need to point it's bucketLastOrPrevious to the last
// entry, and remove its next links.
bucketHead.entry.bucketNext.bucketLastOrPrevious = bucketHead.entry.bucketLastOrPrevious;
bucketHead.entry = bucketHead.entry.bucketNext;
}
} else if (bucketHead.entry == null || entryToRemove.bucketLastOrPrevious == null) {
throw new ConcurrentModificationException();
} else {
if (bucketHead.entry.bucketLastOrPrevious == entryToRemove) {
bucketHead.entry.bucketLastOrPrevious = entryToRemove.bucketLastOrPrevious;
}
entryToRemove.bucketLastOrPrevious.bucketNext = entryToRemove.bucketNext;
if (entryToRemove.bucketNext != null) {
entryToRemove.bucketNext.bucketLastOrPrevious = entryToRemove.bucketLastOrPrevious;
}
}
// Prevent GC nepotism.
entryToRemove.bucketLastOrPrevious = entryToRemove.bucketNext = null;
--size;
}
@Override
public int hashCode() {
if (isEmpty()) {
return 0;
}
int result = HASH_CODE_SEED;
for (final K key : getKeys()) {
result = 31 * result + hashCode(key);
final Iterator extends V> valueItr = getValues(key);
while (valueItr.hasNext()) {
result = 31 * result + hashCodeForValue(valueItr.next());
}
}
return result;
}
@Override
public boolean equals(final Object o) {
if (!(o instanceof MultiMap)) {
return false;
}
@SuppressWarnings("unchecked")
final MultiMap h2 = (MultiMap) o;
if (h2.size() != size()) {
return false;
}
if (this == h2) {
return true;
}
// The regular iterator is not suitable for equality comparisons because the overall ordering is not
// in any specific order relative to the content of this MultiMap.
for (final K key : getKeys()) {
final Iterator extends V> valueItr = getValues(key);
final Iterator extends V> h2ValueItr = h2.getValues(key);
while (valueItr.hasNext() && h2ValueItr.hasNext()) {
if (!equalsForValue(valueItr.next(), h2ValueItr.next())) {
return false;
}
}
if (valueItr.hasNext() != h2ValueItr.hasNext()) {
return false;
}
}
return true;
}
private BucketHead putEntry(@Nullable BucketHead bucketHead, final int keyHash, final int bucketIndex,
final K key, final V value) {
final MultiMapEntry newEntry = newEntry(key, value, keyHash);
if (bucketHead == null) {
bucketHead = new BucketHead<>(lastBucketHead, newEntry);
entries[bucketIndex] = bucketHead;
lastBucketHead = bucketHead;
newEntry.addAsBucketHead();
} else {
newEntry.addToBucketTail(bucketHead);
}
++size;
return bucketHead;
}
private BucketHead putEntry(final int keyHash, final int bucketIndex, final K key, final V value) {
return putEntry(entries[bucketIndex], keyHash, bucketIndex, key, value);
}
private void putAll0(final MultiMap extends K, ? extends V> rhs) {
if (isKeyEqualityCompatible(rhs)) { // Fast path
BucketHead extends K, ? extends V> rhsBucketHead = rhs.lastBucketHead;
while (rhsBucketHead != null) {
MultiMapEntry extends K, ? extends V> rhsEntry = rhsBucketHead.entry;
assert rhsEntry != null;
final int bucketIndex = index(rhsEntry.keyHash);
BucketHead bucketHead = entries[bucketIndex];
if (bucketHead == null) {
bucketHead = putEntry(null, rhsEntry.keyHash, bucketIndex, rhsEntry.getKey(), rhsEntry.getValue());
rhsEntry = rhsEntry.bucketNext;
if (rhsEntry == null) {
rhsBucketHead = rhsBucketHead.prevBucketHead;
continue;
}
}
do {
putEntry(bucketHead, rhsEntry.keyHash, bucketIndex, rhsEntry.getKey(), rhsEntry.getValue());
rhsEntry = rhsEntry.bucketNext;
} while (rhsEntry != null);
rhsBucketHead = rhsBucketHead.prevBucketHead;
}
} else {
BucketHead extends K, ? extends V> rhsBucketHead = rhs.lastBucketHead;
while (rhsBucketHead != null) {
MultiMapEntry extends K, ? extends V> rhsEntry = rhsBucketHead.entry;
assert rhsEntry != null;
do {
putEntry(rhsEntry.keyHash, index(rhsEntry.keyHash), rhsEntry.getKey(), rhsEntry.getValue());
rhsEntry = rhsEntry.bucketNext;
} while (rhsEntry != null);
rhsBucketHead = rhsBucketHead.prevBucketHead;
}
}
}
@Internal
protected abstract class EntryIterator implements Iterator {
@Nullable
private MultiMapEntry previous;
@Nullable
private BucketHead currentBucketHead;
@Nullable
private MultiMapEntry current;
EntryIterator(final BucketHead lastBucketHead) {
currentBucketHead = lastBucketHead;
current = lastBucketHead.entry;
}
@Override
public boolean hasNext() {
return current != null;
}
/**
* @return The object that would be returned by a following {@link #next()} call, or {@code null}.
*/
public T peekNext() {
return current == null ? null : extractNextFromEntry(current);
}
@Override
public T next() {
if (current == null) {
throw new NoSuchElementException();
}
previous = current;
if (current.bucketNext == null) {
assert currentBucketHead != null;
currentBucketHead = currentBucketHead.prevBucketHead;
current = currentBucketHead == null ? null : currentBucketHead.entry;
} else {
current = current.bucketNext;
}
return extractNextFromEntry(previous);
}
@Override
public void remove() {
if (previous == null) {
throw new IllegalStateException();
}
final int i = index(previous.keyHash);
removeEntry(entries[i], previous, i);
previous = null;
}
abstract T extractNextFromEntry(MultiMapEntry entry);
}
private final class FullEntryIterator extends EntryIterator> {
FullEntryIterator(final BucketHead lastBucketHead) {
super(lastBucketHead);
}
@Override
Entry extractNextFromEntry(final MultiMapEntry entry) {
return entry;
}
}
private final class ValueEntryIterator extends EntryIterator {
ValueEntryIterator(final BucketHead lastBucketHead) {
super(lastBucketHead);
}
@Override
V extractNextFromEntry(final MultiMapEntry entry) {
return entry.value;
}
}
private final class ValuesByNameIterator implements Iterator {
final int keyHashCode;
final K key;
@Nullable
private MultiMapEntry current;
@Nullable
private MultiMapEntry previous;
ValuesByNameIterator(final int keyHashCode, final K key, final @Nullable MultiMapEntry first) {
this.keyHashCode = keyHashCode;
this.key = key;
current = first;
}
@Override
public boolean hasNext() {
return current != null;
}
@Override
public V next() {
if (current == null) {
throw new NoSuchElementException();
}
previous = current;
current = findNext(current.bucketNext);
return previous.value;
}
@Override
public void remove() {
if (previous == null) {
throw new IllegalStateException();
}
final int i = index(keyHashCode);
removeEntry(entries[i], previous, i);
previous = null;
}
@Nullable
private MultiMapEntry findNext(@Nullable MultiMapEntry entry) {
while (entry != null) {
if (entry.keyHash == keyHashCode && MultiMap.this.equals(key, entry.getKey())) {
return entry;
}
entry = entry.bucketNext;
}
return null;
}
}
static final class BucketHead {
@Nullable
BucketHead prevBucketHead;
@Nullable
BucketHead nextBucketHead;
MultiMapEntry entry;
BucketHead(@Nullable final BucketHead prevBucketHead, final @Nullable MultiMapEntry entry) {
this.prevBucketHead = prevBucketHead;
if (prevBucketHead != null) {
prevBucketHead.nextBucketHead = this;
}
this.entry = entry;
}
}
static final class MultiMapEntry implements Entry {
final int keyHash;
private final K key;
V value;
/**
* In bucket linked list pointing to the next item in the bucket.
*/
@Nullable
MultiMapEntry bucketNext;
/**
* In bucket linked list with a conditional purpose.
* If this entry is the bucket head then this points to the last entry in the bucket.
* If this entry is NOT the bucket head then it points to the previous entry in the bucket.
*
* This exists so we can do constant time in order bucket insertions and removals.
*/
@Nullable
MultiMapEntry bucketLastOrPrevious;
MultiMapEntry(final K key, final V value, final int keyHash) {
this.key = requireNonNull(key);
this.value = requireNonNull(value);
this.keyHash = keyHash;
}
void addToBucketTail(final BucketHead bucketHead) {
assert bucketHead.entry != null;
assert bucketHead.entry.bucketLastOrPrevious != null;
bucketLastOrPrevious = bucketHead.entry.bucketLastOrPrevious;
bucketHead.entry.bucketLastOrPrevious.bucketNext = this;
bucketHead.entry.bucketLastOrPrevious = this;
}
void addAsBucketHead() {
bucketLastOrPrevious = this;
}
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return value;
}
@Override
public V setValue(final V value) {
requireNonNull(value);
final V oldValue = this.value;
this.value = value;
return oldValue;
}
@Override
public String toString() {
return getKey() + "=" + value;
}
@Override
public boolean equals(final Object o) {
if (!(o instanceof Entry)) {
return false;
}
final Entry, ?> other = (Entry, ?>) o;
return getKey().equals(other.getKey()) && value.equals(other.getValue());
}
@Override
public int hashCode() {
return getKey().hashCode() ^ value.hashCode();
}
}
}