DefaultHeaders value iterator

Motivation:
The Headers interface supports an interface to get all the headers values corresponding to a particular name. This API returns a List which requires intermediate storage and increases GC pressure.

Modifications:
- Add a method which returns an iterator over all the values for a specific name

Result:
Ability to iterator over values for a specific name with no intermediate collection.
This commit is contained in:
Scott Mitchell 2017-09-15 15:55:30 -07:00
parent b32cd26a96
commit 44bb3b6f3a
6 changed files with 220 additions and 2 deletions

View File

@ -90,6 +90,13 @@ public interface Http2Headers extends Headers<CharSequence, CharSequence, Http2H
@Override @Override
Iterator<Entry<CharSequence, CharSequence>> iterator(); Iterator<Entry<CharSequence, CharSequence>> iterator();
/**
* Equivalent to {@link #getAll(Object)} but no intermediate list is generated.
* @param name the name of the header to retrieve
* @return an {@link Iterator} of header values corresponding to {@code name}.
*/
Iterator<CharSequence> valueIterator(CharSequence name);
/** /**
* Sets the {@link PseudoHeaderName#METHOD} header or {@code null} if there is no such header * Sets the {@link PseudoHeaderName#METHOD} header or {@code null} if there is no such header
*/ */

View File

@ -714,6 +714,11 @@ public final class ReadOnlyHttp2Headers implements Http2Headers {
return new ReadOnlyIterator(); return new ReadOnlyIterator();
} }
@Override
public Iterator<CharSequence> valueIterator(CharSequence name) {
return new ReadOnlyValueIterator(name);
}
@Override @Override
public Http2Headers method(CharSequence value) { public Http2Headers method(CharSequence value) {
throw new UnsupportedOperationException("read only"); throw new UnsupportedOperationException("read only");
@ -776,6 +781,58 @@ public final class ReadOnlyHttp2Headers implements Http2Headers {
return builder.append(']').toString(); return builder.append(']').toString();
} }
private final class ReadOnlyValueIterator implements Iterator<CharSequence> {
private int i;
private final int nameHash;
private final CharSequence name;
private AsciiString[] current = pseudoHeaders.length != 0 ? pseudoHeaders : otherHeaders;
private AsciiString next;
ReadOnlyValueIterator(CharSequence name) {
nameHash = AsciiString.hashCode(name);
this.name = name;
calculateNext();
}
@Override
public boolean hasNext() {
return next != null;
}
@Override
public CharSequence next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
CharSequence current = next;
calculateNext();
return current;
}
@Override
public void remove() {
throw new UnsupportedOperationException("read only");
}
private void calculateNext() {
for (; i < current.length; i += 2) {
AsciiString roName = current[i];
if (roName.hashCode() == nameHash && roName.contentEqualsIgnoreCase(name)) {
next = current[i + 1];
i += 2;
return;
}
}
if (i >= current.length && current == pseudoHeaders) {
i = 0;
current = otherHeaders;
calculateNext();
} else {
next = null;
}
}
}
private final class ReadOnlyIterator implements Map.Entry<CharSequence, CharSequence>, private final class ReadOnlyIterator implements Map.Entry<CharSequence, CharSequence>,
Iterator<Map.Entry<CharSequence, CharSequence>> { Iterator<Map.Entry<CharSequence, CharSequence>> {
private int i; private int i;

View File

@ -20,9 +20,14 @@ import org.junit.Test;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.NoSuchElementException;
import static io.netty.handler.codec.http2.DefaultHttp2HeadersTest.verifyPseudoHeadersFirst; import static io.netty.handler.codec.http2.DefaultHttp2HeadersTest.verifyPseudoHeadersFirst;
import static org.junit.Assert.*; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class ReadOnlyHttp2HeadersTest { public class ReadOnlyHttp2HeadersTest {
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
@ -150,6 +155,56 @@ public class ReadOnlyHttp2HeadersTest {
assertNull(headers.get("a missing header")); assertNull(headers.get("a missing header"));
} }
@Test
public void testClientOtherValueIterator() {
testValueIteratorSingleValue(newClientHeaders(), "name2", "value2");
}
@Test
public void testClientPsuedoValueIterator() {
testValueIteratorSingleValue(newClientHeaders(), ":path", "/foo");
}
@Test
public void testServerPsuedoValueIterator() {
testValueIteratorSingleValue(newServerHeaders(), ":status", "200");
}
@Test
public void testEmptyValueIterator() {
Http2Headers headers = newServerHeaders();
Iterator<CharSequence> itr = headers.valueIterator("foo");
assertFalse(itr.hasNext());
try {
itr.next();
fail();
} catch (NoSuchElementException ignored) {
// ignored
}
}
@Test
public void testIteratorMultipleValues() {
Http2Headers headers = ReadOnlyHttp2Headers.serverHeaders(false, new AsciiString("200"), new AsciiString[] {
new AsciiString("name2"), new AsciiString("value1"),
new AsciiString("name1"), new AsciiString("value2"),
new AsciiString("name2"), new AsciiString("value3")
});
Iterator<CharSequence> itr = headers.valueIterator("name2");
assertTrue(itr.hasNext());
assertTrue(AsciiString.contentEqualsIgnoreCase("value1", itr.next()));
assertTrue(itr.hasNext());
assertTrue(AsciiString.contentEqualsIgnoreCase("value3", itr.next()));
assertFalse(itr.hasNext());
}
private void testValueIteratorSingleValue(Http2Headers headers, CharSequence name, CharSequence value) {
Iterator<CharSequence> itr = headers.valueIterator(name);
assertTrue(itr.hasNext());
assertTrue(AsciiString.contentEqualsIgnoreCase(value, itr.next()));
assertFalse(itr.hasNext());
}
private void testIteratorReadOnly(Http2Headers headers) { private void testIteratorReadOnly(Http2Headers headers) {
Iterator<Map.Entry<CharSequence, CharSequence>> itr = headers.iterator(); Iterator<Map.Entry<CharSequence, CharSequence>> itr = headers.iterator();
assertTrue(itr.hasNext()); assertTrue(itr.hasNext());

View File

@ -30,8 +30,8 @@ import java.util.Set;
import static io.netty.util.HashingStrategy.JAVA_HASHER; import static io.netty.util.HashingStrategy.JAVA_HASHER;
import static io.netty.util.internal.MathUtil.findNextPositivePowerOfTwo; import static io.netty.util.internal.MathUtil.findNextPositivePowerOfTwo;
import static io.netty.util.internal.ObjectUtil.checkNotNull; import static io.netty.util.internal.ObjectUtil.checkNotNull;
import static java.lang.Math.min;
import static java.lang.Math.max; import static java.lang.Math.max;
import static java.lang.Math.min;
/** /**
* Default implementation of {@link Headers}; * Default implementation of {@link Headers};
@ -174,6 +174,15 @@ public class DefaultHeaders<K, V, T extends Headers<K, V, T>> implements Headers
return values; return values;
} }
/**
* Equivalent to {@link #getAll(Object)} but no intermediate list is generated.
* @param name the name of the header to retrieve
* @return an {@link Iterator} of header values corresponding to {@code name}.
*/
public Iterator<V> valueIterator(K name) {
return new ValueIterator(name);
}
@Override @Override
public List<V> getAllAndRemove(K name) { public List<V> getAllAndRemove(K name) {
List<V> all = getAll(name); List<V> all = getAll(name);
@ -978,6 +987,49 @@ public class DefaultHeaders<K, V, T extends Headers<K, V, T>> implements Headers
} }
} }
private final class ValueIterator implements Iterator<V> {
private final K name;
private final int hash;
private HeaderEntry<K, V> next;
ValueIterator(K name) {
this.name = checkNotNull(name, "name");
hash = hashingStrategy.hashCode(name);
calculateNext(entries[index(hash)]);
}
@Override
public boolean hasNext() {
return next != null;
}
@Override
public V next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
HeaderEntry<K, V> current = next;
calculateNext(next.next);
return current.value;
}
@Override
public void remove() {
throw new UnsupportedOperationException("read only");
}
private void calculateNext(HeaderEntry<K, V> entry) {
while (entry != null) {
if (entry.hash == hash && hashingStrategy.equals(name, entry.key)) {
next = entry;
return;
}
entry = entry.next;
}
next = null;
}
}
protected static class HeaderEntry<K, V> implements Map.Entry<K, V> { protected static class HeaderEntry<K, V> implements Map.Entry<K, V> {
protected final int hash; protected final int hash;
protected final K key; protected final K key;

View File

@ -483,6 +483,16 @@ public class EmptyHeaders<K, V, T extends Headers<K, V, T>> implements Headers<K
return thisT(); return thisT();
} }
/**
* Equivalent to {@link #getAll(Object)} but no intermediate list is generated.
* @param name the name of the header to retrieve
* @return an {@link Iterator} of header values corresponding to {@code name}.
*/
public Iterator<V> valueIterator(@SuppressWarnings("unused") K name) {
List<V> empty = Collections.emptyList();
return empty.iterator();
}
@Override @Override
public Iterator<Entry<K, V>> iterator() { public Iterator<Entry<K, V>> iterator() {
List<Entry<K, V>> empty = Collections.emptyList(); List<Entry<K, V>> empty = Collections.emptyList();

View File

@ -16,6 +16,7 @@ package io.netty.handler.codec;
import org.junit.Test; import org.junit.Test;
import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -30,6 +31,7 @@ import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/** /**
* Tests for {@link DefaultHeaders}. * Tests for {@link DefaultHeaders}.
@ -102,6 +104,41 @@ public class DefaultHeadersTest {
assertTrue(values.containsAll(asList(of("value1"), of("value2"), of("value3")))); assertTrue(values.containsAll(asList(of("value1"), of("value2"), of("value3"))));
} }
@Test
public void multipleValuesPerNameIterator() {
TestDefaultHeaders headers = newInstance();
headers.add(of("name"), of("value1"));
headers.add(of("name"), of("value2"));
headers.add(of("name"), of("value3"));
assertEquals(3, headers.size());
List<CharSequence> values = new ArrayList<CharSequence>();
Iterator<CharSequence> itr = headers.valueIterator(of("name"));
while (itr.hasNext()) {
values.add(itr.next());
}
assertEquals(3, values.size());
assertTrue(values.containsAll(asList(of("value1"), of("value2"), of("value3"))));
}
@Test
public void multipleValuesPerNameIteratorEmpty() {
TestDefaultHeaders headers = newInstance();
List<CharSequence> values = new ArrayList<CharSequence>();
Iterator<CharSequence> itr = headers.valueIterator(of("name"));
while (itr.hasNext()) {
values.add(itr.next());
}
assertEquals(0, values.size());
try {
itr.next();
fail();
} catch (NoSuchElementException ignored) {
// ignored
}
}
@Test @Test
public void testContains() { public void testContains() {
TestDefaultHeaders headers = newInstance(); TestDefaultHeaders headers = newInstance();