Add unescapeCsvFields to parse a CSV line and implement CombinedHttpHeaders.getAll

Motivation:

See #4855

Modifications:

Unfortunately, unescapeCsv cannot be used here because the input could be a CSV line like `"a,b",c`. Hence this patch adds unescapeCsvFields to parse a CSV line and split it into multiple fields and unescaped them. The unit tests should define the behavior of unescapeCsvFields.

Then this patch just uses unescapeCsvFields to implement `CombinedHttpHeaders.getAll`.

Result:

`CombinedHttpHeaders.getAll` will return the unescaped values of a header.
This commit is contained in:
Xiaoyan Lin 2016-02-10 20:32:27 -08:00 committed by Scott Mitchell
parent ccb0870600
commit 333f55e9ce
4 changed files with 154 additions and 4 deletions

View File

@ -23,6 +23,7 @@ import io.netty.util.internal.StringUtil;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import static io.netty.util.AsciiString.CASE_INSENSITIVE_HASHER;
@ -77,6 +78,18 @@ public class CombinedHttpHeaders extends DefaultHttpHeaders {
super(nameHashingStrategy, valueConverter, nameValidator);
}
@Override
public List<CharSequence> getAll(CharSequence name) {
List<CharSequence> values = super.getAll(name);
if (values.isEmpty()) {
return values;
}
if (values.size() != 1) {
throw new IllegalStateException("CombinedHttpHeaders should only have one value");
}
return StringUtil.unescapeCsvFields(values.get(0));
}
@Override
public CombinedHttpHeadersImpl add(Headers<? extends CharSequence, ? extends CharSequence, ?> headers) {
// Override the fast-copy mechanism used by DefaultHeaders
@ -158,6 +171,12 @@ public class CombinedHttpHeaders extends DefaultHttpHeaders {
return this;
}
@Override
public CombinedHttpHeadersImpl setObject(CharSequence name, Object value) {
super.set(name, commaSeparate(objectEscaper(), value));
return this;
}
@Override
public CombinedHttpHeadersImpl setObject(CharSequence name, Object... values) {
super.set(name, commaSeparate(objectEscaper(), values));

View File

@ -18,6 +18,7 @@ package io.netty.handler.codec.http;
import io.netty.handler.codec.http.HttpHeadersTestUtils.HeaderValue;
import org.junit.Test;
import java.util.Arrays;
import java.util.Collections;
import static io.netty.util.AsciiString.contentEquals;
@ -101,7 +102,7 @@ public class CombinedHttpHeadersTest {
final CombinedHttpHeaders headers = newCombinedHttpHeaders();
headers.add(HEADER_NAME, HeaderValue.SIX_QUOTED.subset(4));
assertTrue(contentEquals(HeaderValue.SIX_QUOTED.subsetAsCsvString(4), headers.get(HEADER_NAME)));
assertTrue(contentEquals(HeaderValue.SIX_QUOTED.subsetAsCsvString(4), headers.getAll(HEADER_NAME).get(0)));
assertEquals(HeaderValue.SIX_QUOTED.subset(4), headers.getAll(HEADER_NAME));
}
@Test
@ -109,7 +110,7 @@ public class CombinedHttpHeadersTest {
final CombinedHttpHeaders headers = newCombinedHttpHeaders();
headers.add(HEADER_NAME, HeaderValue.EIGHT.subset(6));
assertTrue(contentEquals(HeaderValue.EIGHT.subsetAsCsvString(6), headers.get(HEADER_NAME)));
assertTrue(contentEquals(HeaderValue.EIGHT.subsetAsCsvString(6), headers.getAll(HEADER_NAME).get(0)));
assertEquals(HeaderValue.EIGHT.subset(6), headers.getAll(HEADER_NAME));
}
@Test (expected = NullPointerException.class)
@ -168,7 +169,7 @@ public class CombinedHttpHeadersTest {
public void addIterableCsvEmtpy() {
final CombinedHttpHeaders headers = newCombinedHttpHeaders();
headers.add(HEADER_NAME, Collections.<CharSequence>emptyList());
assertTrue(contentEquals("", headers.getAll(HEADER_NAME).get(0)));
assertEquals(Arrays.asList(""), headers.getAll(HEADER_NAME));
}
@Test
@ -234,7 +235,7 @@ public class CombinedHttpHeadersTest {
private static void assertCsvValues(final CombinedHttpHeaders headers, final HeaderValue headerValue) {
assertTrue(contentEquals(headerValue.asCsv(), headers.get(HEADER_NAME)));
assertTrue(contentEquals(headerValue.asCsv(), headers.getAll(HEADER_NAME).get(0)));
assertEquals(headerValue.asList(), headers.getAll(HEADER_NAME));
}
private static void assertCsvValue(final CombinedHttpHeaders headers, final HeaderValue headerValue) {
@ -253,4 +254,21 @@ public class CombinedHttpHeadersTest {
headers.add(HEADER_NAME, v.toString());
}
}
@Test
public void testGetAll() {
final CombinedHttpHeaders headers = newCombinedHttpHeaders();
headers.set(HEADER_NAME, Arrays.asList("a", "b", "c"));
assertEquals(Arrays.asList("a", "b", "c"), headers.getAll(HEADER_NAME));
headers.set(HEADER_NAME, Arrays.asList("a,", "b,", "c,"));
assertEquals(Arrays.asList("a,", "b,", "c,"), headers.getAll(HEADER_NAME));
headers.set(HEADER_NAME, Arrays.asList("a\"", "b\"", "c\""));
assertEquals(Arrays.asList("a\"", "b\"", "c\""), headers.getAll(HEADER_NAME));
headers.set(HEADER_NAME, Arrays.asList("\"a\"", "\"b\"", "\"c\""));
assertEquals(Arrays.asList("a", "b", "c"), headers.getAll(HEADER_NAME));
headers.set(HEADER_NAME, "a,b,c");
assertEquals(Arrays.asList("a,b,c"), headers.getAll(HEADER_NAME));
headers.set(HEADER_NAME, "\"a,b,c\"");
assertEquals(Arrays.asList("a,b,c"), headers.getAll(HEADER_NAME));
}
}

View File

@ -16,6 +16,7 @@
package io.netty.util.internal;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.List;
@ -425,6 +426,76 @@ public final class StringUtil {
}
/**
* Unescapes the specified escaped CSV fields according to
* <a href="https://tools.ietf.org/html/rfc4180#section-2">RFC-4180</a>.
*
* @param value A string with multiple CSV escaped fields which will be unescaped according to
* <a href="https://tools.ietf.org/html/rfc4180#section-2">RFC-4180</a>
* @return {@link List} the list of unescaped fields
*/
public static List<CharSequence> unescapeCsvFields(CharSequence value) {
List<CharSequence> unescaped = new ArrayList<CharSequence>(2);
StringBuilder current = InternalThreadLocalMap.get().stringBuilder();
boolean quoted = false;
int last = value.length() - 1;
for (int i = 0; i <= last; i++) {
char c = value.charAt(i);
if (quoted) {
switch (c) {
case DOUBLE_QUOTE:
if (i == last) {
// Add the last field and return
unescaped.add(current.toString());
return unescaped;
}
char next = value.charAt(++i);
if (next == DOUBLE_QUOTE) {
// 2 double-quotes should be unescaped to one
current.append(DOUBLE_QUOTE);
break;
}
if (next == COMMA) {
// This is the end of a field. Let's start to parse the next field.
quoted = false;
unescaped.add(current.toString());
current.setLength(0);
break;
}
// double-quote followed by other character is invalid
throw newInvalidEscapedCsvFieldException(value, i - 1);
default:
current.append(c);
}
} else {
switch (c) {
case COMMA:
// Start to parse the next field
unescaped.add(current.toString());
current.setLength(0);
break;
case DOUBLE_QUOTE:
if (current.length() == 0) {
quoted = true;
break;
}
// double-quote appears without being enclosed with double-quotes
case LINE_FEED:
case CARRIAGE_RETURN:
// special characters appears without being enclosed with double-quotes
throw newInvalidEscapedCsvFieldException(value, i);
default:
current.append(c);
}
}
}
if (quoted) {
throw newInvalidEscapedCsvFieldException(value, last);
}
unescaped.add(current.toString());
return unescaped;
}
/**s
* Validate if {@code value} is a valid csv field without double-quotes.
*
* @throws IllegalArgumentException if {@code value} needs to be encoded with double-quotes.

View File

@ -15,6 +15,7 @@
*/
package io.netty.util.internal;
import java.util.Arrays;
import org.junit.Test;
import static io.netty.util.internal.StringUtil.*;
@ -376,6 +377,47 @@ public class StringUtilTest {
assertEquals(value, unescapeCsv(StringUtil.escapeCsv(value)));
}
@Test
public void testUnescapeCsvFields() {
assertEquals(Arrays.asList(""), unescapeCsvFields(""));
assertEquals(Arrays.asList("", ""), unescapeCsvFields(","));
assertEquals(Arrays.asList("a", ""), unescapeCsvFields("a,"));
assertEquals(Arrays.asList("", "a"), unescapeCsvFields(",a"));
assertEquals(Arrays.asList("\""), unescapeCsvFields("\"\"\"\""));
assertEquals(Arrays.asList("\"", "\""), unescapeCsvFields("\"\"\"\",\"\"\"\""));
assertEquals(Arrays.asList("netty"), unescapeCsvFields("netty"));
assertEquals(Arrays.asList("hello", "netty"), unescapeCsvFields("hello,netty"));
assertEquals(Arrays.asList("hello,netty"), unescapeCsvFields("\"hello,netty\""));
assertEquals(Arrays.asList("hello", "netty"), unescapeCsvFields("\"hello\",\"netty\""));
assertEquals(Arrays.asList("a\"b", "c\"d"), unescapeCsvFields("\"a\"\"b\",\"c\"\"d\""));
assertEquals(Arrays.asList("a\rb", "c\nd"), unescapeCsvFields("\"a\rb\",\"c\nd\""));
}
@Test(expected = IllegalArgumentException.class)
public void unescapeCsvFieldsWithCRWithoutQuote() {
unescapeCsvFields("a,\r");
}
@Test(expected = IllegalArgumentException.class)
public void unescapeCsvFieldsWithLFWithoutQuote() {
unescapeCsvFields("a,\r");
}
@Test(expected = IllegalArgumentException.class)
public void unescapeCsvFieldsWithQuote() {
unescapeCsvFields("a,\"");
}
@Test(expected = IllegalArgumentException.class)
public void unescapeCsvFieldsWithQuote2() {
unescapeCsvFields("\",a");
}
@Test(expected = IllegalArgumentException.class)
public void unescapeCsvFieldsWithQuote3() {
unescapeCsvFields("a\"b,a");
}
@Test
public void testSimpleClassName() throws Exception {
testSimpleClassName(String.class);