HttpHeaders valuesIterator and contains improvements

Motivation:
In order to determine if a header contains a value we currently rely
upon getAll(..) and regular expressions. This operation is commonly used
during the encode and decode stage to determine the transfer encoding
(e.g. HttpUtil#isTransferEncodingChunked). This operation requires an
intermediate collection and possibly regular expressions for the
CombinedHttpHeaders use case which can be expensive.

Modifications:
- Add a valuesIterator to HttpHeaders and specializations of this method
for DefaultHttpHeaders, ReadOnlyHttpHeaders, and CombinedHttpHeaders.

Result:
Less intermediate collections and allocation overhead when determining
if HttpHeaders contains a name/value pair.
This commit is contained in:
Scott Mitchell 2017-11-11 18:22:22 -08:00
parent e6126215e0
commit 0a47c590fe
6 changed files with 285 additions and 27 deletions

View File

@ -28,6 +28,7 @@ import java.util.Map;
import static io.netty.util.AsciiString.CASE_INSENSITIVE_HASHER;
import static io.netty.util.internal.StringUtil.COMMA;
import static io.netty.util.internal.StringUtil.unescapeCsvFields;
/**
* Will add multiple values for the same header as single header with a comma separated list of values.
@ -83,6 +84,19 @@ public class CombinedHttpHeaders extends DefaultHttpHeaders {
super(nameHashingStrategy, valueConverter, nameValidator);
}
@Override
public Iterator<CharSequence> valueIterator(CharSequence name) {
Iterator<CharSequence> itr = super.valueIterator(name);
if (!itr.hasNext()) {
return itr;
}
Iterator<CharSequence> unescapedItr = unescapeCsvFields(itr.next()).iterator();
if (itr.hasNext()) {
throw new IllegalStateException("CombinedHttpHeaders should only have one value");
}
return unescapedItr;
}
@Override
public List<CharSequence> getAll(CharSequence name) {
List<CharSequence> values = super.getAll(name);
@ -92,7 +106,7 @@ public class CombinedHttpHeaders extends DefaultHttpHeaders {
if (values.size() != 1) {
throw new IllegalStateException("CombinedHttpHeaders should only have one value");
}
return StringUtil.unescapeCsvFields(values.get(0));
return unescapeCsvFields(values.get(0));
}
@Override

View File

@ -276,6 +276,32 @@ public class DefaultHttpHeaders extends HttpHeaders {
return headers.iterator();
}
@Override
public Iterator<String> valueStringIterator(CharSequence name) {
final Iterator<CharSequence> itr = valueCharSequenceIterator(name);
return new Iterator<String>() {
@Override
public boolean hasNext() {
return itr.hasNext();
}
@Override
public String next() {
return itr.next().toString();
}
@Override
public void remove() {
itr.remove();
}
};
}
@Override
public Iterator<CharSequence> valueCharSequenceIterator(CharSequence name) {
return headers.valueIterator(name);
}
@Override
public boolean contains(String name) {
return contains((CharSequence) name);

View File

@ -31,6 +31,9 @@ import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import static io.netty.util.AsciiString.contentEquals;
import static io.netty.util.AsciiString.contentEqualsIgnoreCase;
import static io.netty.util.AsciiString.trim;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
/**
@ -1146,7 +1149,7 @@ public abstract class HttpHeaders implements Iterable<Map.Entry<String, String>>
*/
@Deprecated
public static boolean equalsIgnoreCase(CharSequence name1, CharSequence name2) {
return AsciiString.contentEqualsIgnoreCase(name1, name2);
return contentEqualsIgnoreCase(name1, name2);
}
@Deprecated
@ -1309,6 +1312,24 @@ public abstract class HttpHeaders implements Iterable<Map.Entry<String, String>>
*/
public abstract Iterator<Entry<CharSequence, CharSequence>> iteratorCharSequence();
/**
* Equivalent to {@link #getAll(String)} but it is possible that 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<String> valueStringIterator(CharSequence name) {
return getAll(name).iterator();
}
/**
* Equivalent to {@link #getAll(String)} but it is possible that 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<? extends CharSequence> valueCharSequenceIterator(CharSequence name) {
return valueStringIterator(name);
}
/**
* Checks to see if there is a header with the specified name
*
@ -1546,18 +1567,16 @@ public abstract class HttpHeaders implements Iterable<Map.Entry<String, String>>
* @see #contains(CharSequence, CharSequence, boolean)
*/
public boolean contains(String name, String value, boolean ignoreCase) {
List<String> values = getAll(name);
if (values.isEmpty()) {
return false;
}
for (String v: values) {
if (ignoreCase) {
if (v.equalsIgnoreCase(value)) {
Iterator<String> valueIterator = valueStringIterator(name);
if (ignoreCase) {
while (valueIterator.hasNext()) {
if (valueIterator.next().equalsIgnoreCase(value)) {
return true;
}
} else {
if (v.equals(value)) {
}
} else {
while (valueIterator.hasNext()) {
if (valueIterator.next().equals(value)) {
return true;
}
}
@ -1576,32 +1595,56 @@ public abstract class HttpHeaders implements Iterable<Map.Entry<String, String>>
* otherwise a case sensitive compare is run to compare values.
*/
public boolean containsValue(CharSequence name, CharSequence value, boolean ignoreCase) {
List<String> values = getAll(name);
if (values.isEmpty()) {
return false;
}
for (String v: values) {
if (contains(v, value, ignoreCase)) {
Iterator<? extends CharSequence> itr = valueCharSequenceIterator(name);
while (itr.hasNext()) {
if (containsCommaSeparatedTrimmed(itr.next(), value, ignoreCase)) {
return true;
}
}
return false;
}
private static boolean contains(String value, CharSequence expected, boolean ignoreCase) {
String[] parts = value.split(",");
private static boolean containsCommaSeparatedTrimmed(CharSequence rawNext, CharSequence expected,
boolean ignoreCase) {
int begin = 0;
int end;
if (ignoreCase) {
for (String s: parts) {
if (AsciiString.contentEqualsIgnoreCase(expected, s.trim())) {
if ((end = AsciiString.indexOf(rawNext, ',', begin)) == -1) {
if (contentEqualsIgnoreCase(trim(rawNext), expected)) {
return true;
}
} else {
do {
if (contentEqualsIgnoreCase(trim(rawNext.subSequence(begin, end)), expected)) {
return true;
}
begin = end + 1;
} while ((end = AsciiString.indexOf(rawNext, ',', begin)) != -1);
if (begin < rawNext.length()) {
if (contentEqualsIgnoreCase(trim(rawNext.subSequence(begin, rawNext.length())), expected)) {
return true;
}
}
}
} else {
for (String s: parts) {
if (AsciiString.contentEquals(expected, s.trim())) {
if ((end = AsciiString.indexOf(rawNext, ',', begin)) == -1) {
if (contentEquals(trim(rawNext), expected)) {
return true;
}
} else {
do {
if (contentEquals(trim(rawNext.subSequence(begin, end)), expected)) {
return true;
}
begin = end + 1;
} while ((end = AsciiString.indexOf(rawNext, ',', begin)) != -1);
if (begin < rawNext.length()) {
if (contentEquals(trim(rawNext.subSequence(begin, rawNext.length())), expected)) {
return true;
}
}
}
}
return false;

View File

@ -30,6 +30,7 @@ import java.util.Set;
import static io.netty.handler.codec.CharSequenceValueConverter.INSTANCE;
import static io.netty.handler.codec.http.DefaultHttpHeaders.HttpNameValidator;
import static io.netty.util.AsciiString.contentEquals;
import static io.netty.util.AsciiString.contentEqualsIgnoreCase;
/**
@ -76,7 +77,7 @@ public final class ReadOnlyHttpHeaders extends HttpHeaders {
final int nameHash = AsciiString.hashCode(name);
for (int i = 0; i < nameValuePairs.length; i += 2) {
CharSequence roName = nameValuePairs[i];
if (roName.hashCode() == nameHash && contentEqualsIgnoreCase(roName, name)) {
if (AsciiString.hashCode(roName) == nameHash && contentEqualsIgnoreCase(roName, name)) {
return nameValuePairs[i + 1];
}
}
@ -134,7 +135,7 @@ public final class ReadOnlyHttpHeaders extends HttpHeaders {
List<String> values = new ArrayList<String>(4);
for (int i = 0; i < nameValuePairs.length; i += 2) {
CharSequence roName = nameValuePairs[i];
if (roName.hashCode() == nameHash && contentEqualsIgnoreCase(roName, name)) {
if (AsciiString.hashCode(roName) == nameHash && contentEqualsIgnoreCase(roName, name)) {
values.add(nameValuePairs[i + 1].toString());
}
}
@ -159,6 +160,41 @@ public final class ReadOnlyHttpHeaders extends HttpHeaders {
return get0(name) != null;
}
@Override
public boolean contains(String name, String value, boolean ignoreCase) {
return containsValue(name, value, ignoreCase);
}
@Override
public boolean containsValue(CharSequence name, CharSequence value, boolean ignoreCase) {
if (ignoreCase) {
for (int i = 0; i < nameValuePairs.length; i += 2) {
if (contentEqualsIgnoreCase(nameValuePairs[i], name) &&
contentEqualsIgnoreCase(nameValuePairs[i + 1], value)) {
return true;
}
}
} else {
for (int i = 0; i < nameValuePairs.length; i += 2) {
if (contentEqualsIgnoreCase(nameValuePairs[i], name) &&
contentEquals(nameValuePairs[i + 1], value)) {
return true;
}
}
}
return false;
}
@Override
public Iterator<String> valueStringIterator(CharSequence name) {
return new ReadOnlyStringValueIterator(name);
}
@Override
public Iterator<CharSequence> valueCharSequenceIterator(CharSequence name) {
return new ReadOnlyValueIterator(name);
}
@Override
public Iterator<Map.Entry<String, String>> iterator() {
return new ReadOnlyStringIterator();
@ -336,4 +372,88 @@ public final class ReadOnlyHttpHeaders extends HttpHeaders {
return key + '=' + value;
}
}
private final class ReadOnlyStringValueIterator implements Iterator<String> {
private final CharSequence name;
private final int nameHash;
private int nextNameIndex;
ReadOnlyStringValueIterator(CharSequence name) {
this.name = name;
nameHash = AsciiString.hashCode(name);
nextNameIndex = findNextValue();
}
@Override
public boolean hasNext() {
return nextNameIndex != -1;
}
@Override
public String next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
String value = nameValuePairs[nextNameIndex + 1].toString();
nextNameIndex = findNextValue();
return value;
}
@Override
public void remove() {
throw new UnsupportedOperationException("read only");
}
private int findNextValue() {
for (int i = nextNameIndex; i < nameValuePairs.length; i += 2) {
final CharSequence roName = nameValuePairs[i];
if (nameHash == AsciiString.hashCode(roName) && contentEqualsIgnoreCase(name, roName)) {
return i;
}
}
return -1;
}
}
private final class ReadOnlyValueIterator implements Iterator<CharSequence> {
private final CharSequence name;
private final int nameHash;
private int nextNameIndex;
ReadOnlyValueIterator(CharSequence name) {
this.name = name;
nameHash = AsciiString.hashCode(name);
nextNameIndex = findNextValue();
}
@Override
public boolean hasNext() {
return nextNameIndex != -1;
}
@Override
public CharSequence next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
CharSequence value = nameValuePairs[nextNameIndex + 1];
nextNameIndex = findNextValue();
return value;
}
@Override
public void remove() {
throw new UnsupportedOperationException("read only");
}
private int findNextValue() {
for (int i = nextNameIndex; i < nameValuePairs.length; i += 2) {
final CharSequence roName = nameValuePairs[i];
if (nameHash == AsciiString.hashCode(roName) && contentEqualsIgnoreCase(name, roName)) {
return i;
}
}
return -1;
}
}
}

View File

@ -20,6 +20,7 @@ import org.junit.Test;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import static io.netty.util.AsciiString.contentEquals;
import static org.junit.Assert.assertEquals;
@ -300,4 +301,30 @@ public class CombinedHttpHeadersTest {
HttpHeaders copiedHeaders = newCombinedHttpHeaders().add(headers);
assertEquals(Arrays.asList("a", "", "b", "", "c, d"), copiedHeaders.getAll(HEADER_NAME));
}
@Test
public void valueIterator() {
final CombinedHttpHeaders headers = newCombinedHttpHeaders();
headers.set(HEADER_NAME, Arrays.asList("\ta", " ", " b ", "\t \t"));
headers.add(HEADER_NAME, " c, d \t");
assertFalse(headers.valueStringIterator("foo").hasNext());
assertValueIterator(headers.valueStringIterator(HEADER_NAME));
assertFalse(headers.valueCharSequenceIterator("foo").hasNext());
assertValueIterator(headers.valueCharSequenceIterator(HEADER_NAME));
}
private static void assertValueIterator(Iterator<? extends CharSequence> strItr) {
assertTrue(strItr.hasNext());
assertEquals("a", strItr.next());
assertTrue(strItr.hasNext());
assertEquals("", strItr.next());
assertTrue(strItr.hasNext());
assertEquals("b", strItr.next());
assertTrue(strItr.hasNext());
assertEquals("", strItr.next());
assertTrue(strItr.hasNext());
assertEquals("c, d", strItr.next());
assertFalse(strItr.hasNext());
}
}

View File

@ -1005,6 +1005,34 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence>
return new AsciiString(newValue, false);
}
/**
* Copies this string removing white space characters from the beginning and end of the string, and tries not to
* copy if possible.
*
* @param c The {@link CharSequence} to trim.
* @return a new string with characters {@code <= \\u0020} removed from the beginning and the end.
*/
public static CharSequence trim(CharSequence c) {
if (c.getClass() == AsciiString.class) {
return ((AsciiString) c).trim();
}
if (c instanceof String) {
return ((String) c).trim();
}
int start = 0, last = c.length() - 1;
int end = last;
while (start <= end && c.charAt(start) <= ' ') {
start++;
}
while (end >= start && c.charAt(end) <= ' ') {
end--;
}
if (start == 0 && end == last) {
return c;
}
return c.subSequence(start, end);
}
/**
* Duplicates this string removing white space characters from the beginning and end of the
* string, without copying.