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:
parent
e6126215e0
commit
0a47c590fe
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user