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.AsciiString.CASE_INSENSITIVE_HASHER;
import static io.netty.util.internal.StringUtil.COMMA; 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. * 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); 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 @Override
public List<CharSequence> getAll(CharSequence name) { public List<CharSequence> getAll(CharSequence name) {
List<CharSequence> values = super.getAll(name); List<CharSequence> values = super.getAll(name);
@ -92,7 +106,7 @@ public class CombinedHttpHeaders extends DefaultHttpHeaders {
if (values.size() != 1) { if (values.size() != 1) {
throw new IllegalStateException("CombinedHttpHeaders should only have one value"); throw new IllegalStateException("CombinedHttpHeaders should only have one value");
} }
return StringUtil.unescapeCsvFields(values.get(0)); return unescapeCsvFields(values.get(0));
} }
@Override @Override

View File

@ -276,6 +276,32 @@ public class DefaultHttpHeaders extends HttpHeaders {
return headers.iterator(); 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 @Override
public boolean contains(String name) { public boolean contains(String name) {
return contains((CharSequence) name); return contains((CharSequence) name);

View File

@ -31,6 +31,9 @@ import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set; 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; import static io.netty.util.internal.ObjectUtil.checkNotNull;
/** /**
@ -1146,7 +1149,7 @@ public abstract class HttpHeaders implements Iterable<Map.Entry<String, String>>
*/ */
@Deprecated @Deprecated
public static boolean equalsIgnoreCase(CharSequence name1, CharSequence name2) { public static boolean equalsIgnoreCase(CharSequence name1, CharSequence name2) {
return AsciiString.contentEqualsIgnoreCase(name1, name2); return contentEqualsIgnoreCase(name1, name2);
} }
@Deprecated @Deprecated
@ -1309,6 +1312,24 @@ public abstract class HttpHeaders implements Iterable<Map.Entry<String, String>>
*/ */
public abstract Iterator<Entry<CharSequence, CharSequence>> iteratorCharSequence(); 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 * 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) * @see #contains(CharSequence, CharSequence, boolean)
*/ */
public boolean contains(String name, String value, boolean ignoreCase) { public boolean contains(String name, String value, boolean ignoreCase) {
List<String> values = getAll(name); Iterator<String> valueIterator = valueStringIterator(name);
if (values.isEmpty()) { if (ignoreCase) {
return false; while (valueIterator.hasNext()) {
} if (valueIterator.next().equalsIgnoreCase(value)) {
for (String v: values) {
if (ignoreCase) {
if (v.equalsIgnoreCase(value)) {
return true; return true;
} }
} else { }
if (v.equals(value)) { } else {
while (valueIterator.hasNext()) {
if (valueIterator.next().equals(value)) {
return true; 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. * otherwise a case sensitive compare is run to compare values.
*/ */
public boolean containsValue(CharSequence name, CharSequence value, boolean ignoreCase) { public boolean containsValue(CharSequence name, CharSequence value, boolean ignoreCase) {
List<String> values = getAll(name); Iterator<? extends CharSequence> itr = valueCharSequenceIterator(name);
if (values.isEmpty()) { while (itr.hasNext()) {
return false; if (containsCommaSeparatedTrimmed(itr.next(), value, ignoreCase)) {
}
for (String v: values) {
if (contains(v, value, ignoreCase)) {
return true; return true;
} }
} }
return false; return false;
} }
private static boolean contains(String value, CharSequence expected, boolean ignoreCase) { private static boolean containsCommaSeparatedTrimmed(CharSequence rawNext, CharSequence expected,
String[] parts = value.split(","); boolean ignoreCase) {
int begin = 0;
int end;
if (ignoreCase) { if (ignoreCase) {
for (String s: parts) { if ((end = AsciiString.indexOf(rawNext, ',', begin)) == -1) {
if (AsciiString.contentEqualsIgnoreCase(expected, s.trim())) { if (contentEqualsIgnoreCase(trim(rawNext), expected)) {
return true; 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 { } else {
for (String s: parts) { if ((end = AsciiString.indexOf(rawNext, ',', begin)) == -1) {
if (AsciiString.contentEquals(expected, s.trim())) { if (contentEquals(trim(rawNext), expected)) {
return true; 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; 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.CharSequenceValueConverter.INSTANCE;
import static io.netty.handler.codec.http.DefaultHttpHeaders.HttpNameValidator; import static io.netty.handler.codec.http.DefaultHttpHeaders.HttpNameValidator;
import static io.netty.util.AsciiString.contentEquals;
import static io.netty.util.AsciiString.contentEqualsIgnoreCase; import static io.netty.util.AsciiString.contentEqualsIgnoreCase;
/** /**
@ -76,7 +77,7 @@ public final class ReadOnlyHttpHeaders extends HttpHeaders {
final int nameHash = AsciiString.hashCode(name); final int nameHash = AsciiString.hashCode(name);
for (int i = 0; i < nameValuePairs.length; i += 2) { for (int i = 0; i < nameValuePairs.length; i += 2) {
CharSequence roName = nameValuePairs[i]; CharSequence roName = nameValuePairs[i];
if (roName.hashCode() == nameHash && contentEqualsIgnoreCase(roName, name)) { if (AsciiString.hashCode(roName) == nameHash && contentEqualsIgnoreCase(roName, name)) {
return nameValuePairs[i + 1]; return nameValuePairs[i + 1];
} }
} }
@ -134,7 +135,7 @@ public final class ReadOnlyHttpHeaders extends HttpHeaders {
List<String> values = new ArrayList<String>(4); List<String> values = new ArrayList<String>(4);
for (int i = 0; i < nameValuePairs.length; i += 2) { for (int i = 0; i < nameValuePairs.length; i += 2) {
CharSequence roName = nameValuePairs[i]; 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()); values.add(nameValuePairs[i + 1].toString());
} }
} }
@ -159,6 +160,41 @@ public final class ReadOnlyHttpHeaders extends HttpHeaders {
return get0(name) != null; 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 @Override
public Iterator<Map.Entry<String, String>> iterator() { public Iterator<Map.Entry<String, String>> iterator() {
return new ReadOnlyStringIterator(); return new ReadOnlyStringIterator();
@ -336,4 +372,88 @@ public final class ReadOnlyHttpHeaders extends HttpHeaders {
return key + '=' + value; 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.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator;
import static io.netty.util.AsciiString.contentEquals; import static io.netty.util.AsciiString.contentEquals;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@ -300,4 +301,30 @@ public class CombinedHttpHeadersTest {
HttpHeaders copiedHeaders = newCombinedHttpHeaders().add(headers); HttpHeaders copiedHeaders = newCombinedHttpHeaders().add(headers);
assertEquals(Arrays.asList("a", "", "b", "", "c, d"), copiedHeaders.getAll(HEADER_NAME)); 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); 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 * Duplicates this string removing white space characters from the beginning and end of the
* string, without copying. * string, without copying.