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:
parent
ccb0870600
commit
333f55e9ce
|
@ -23,6 +23,7 @@ import io.netty.util.internal.StringUtil;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static io.netty.util.AsciiString.CASE_INSENSITIVE_HASHER;
|
import static io.netty.util.AsciiString.CASE_INSENSITIVE_HASHER;
|
||||||
|
@ -77,6 +78,18 @@ public class CombinedHttpHeaders extends DefaultHttpHeaders {
|
||||||
super(nameHashingStrategy, valueConverter, nameValidator);
|
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
|
@Override
|
||||||
public CombinedHttpHeadersImpl add(Headers<? extends CharSequence, ? extends CharSequence, ?> headers) {
|
public CombinedHttpHeadersImpl add(Headers<? extends CharSequence, ? extends CharSequence, ?> headers) {
|
||||||
// Override the fast-copy mechanism used by DefaultHeaders
|
// Override the fast-copy mechanism used by DefaultHeaders
|
||||||
|
@ -158,6 +171,12 @@ public class CombinedHttpHeaders extends DefaultHttpHeaders {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CombinedHttpHeadersImpl setObject(CharSequence name, Object value) {
|
||||||
|
super.set(name, commaSeparate(objectEscaper(), value));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CombinedHttpHeadersImpl setObject(CharSequence name, Object... values) {
|
public CombinedHttpHeadersImpl setObject(CharSequence name, Object... values) {
|
||||||
super.set(name, commaSeparate(objectEscaper(), values));
|
super.set(name, commaSeparate(objectEscaper(), values));
|
||||||
|
|
|
@ -18,6 +18,7 @@ package io.netty.handler.codec.http;
|
||||||
import io.netty.handler.codec.http.HttpHeadersTestUtils.HeaderValue;
|
import io.netty.handler.codec.http.HttpHeadersTestUtils.HeaderValue;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
||||||
import static io.netty.util.AsciiString.contentEquals;
|
import static io.netty.util.AsciiString.contentEquals;
|
||||||
|
@ -101,7 +102,7 @@ public class CombinedHttpHeadersTest {
|
||||||
final CombinedHttpHeaders headers = newCombinedHttpHeaders();
|
final CombinedHttpHeaders headers = newCombinedHttpHeaders();
|
||||||
headers.add(HEADER_NAME, HeaderValue.SIX_QUOTED.subset(4));
|
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.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
|
@Test
|
||||||
|
@ -109,7 +110,7 @@ public class CombinedHttpHeadersTest {
|
||||||
final CombinedHttpHeaders headers = newCombinedHttpHeaders();
|
final CombinedHttpHeaders headers = newCombinedHttpHeaders();
|
||||||
headers.add(HEADER_NAME, HeaderValue.EIGHT.subset(6));
|
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.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)
|
@Test (expected = NullPointerException.class)
|
||||||
|
@ -168,7 +169,7 @@ public class CombinedHttpHeadersTest {
|
||||||
public void addIterableCsvEmtpy() {
|
public void addIterableCsvEmtpy() {
|
||||||
final CombinedHttpHeaders headers = newCombinedHttpHeaders();
|
final CombinedHttpHeaders headers = newCombinedHttpHeaders();
|
||||||
headers.add(HEADER_NAME, Collections.<CharSequence>emptyList());
|
headers.add(HEADER_NAME, Collections.<CharSequence>emptyList());
|
||||||
assertTrue(contentEquals("", headers.getAll(HEADER_NAME).get(0)));
|
assertEquals(Arrays.asList(""), headers.getAll(HEADER_NAME));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -234,7 +235,7 @@ public class CombinedHttpHeadersTest {
|
||||||
|
|
||||||
private static void assertCsvValues(final CombinedHttpHeaders headers, final HeaderValue headerValue) {
|
private static void assertCsvValues(final CombinedHttpHeaders headers, final HeaderValue headerValue) {
|
||||||
assertTrue(contentEquals(headerValue.asCsv(), headers.get(HEADER_NAME)));
|
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) {
|
private static void assertCsvValue(final CombinedHttpHeaders headers, final HeaderValue headerValue) {
|
||||||
|
@ -253,4 +254,21 @@ public class CombinedHttpHeadersTest {
|
||||||
headers.add(HEADER_NAME, v.toString());
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
package io.netty.util.internal;
|
package io.netty.util.internal;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Formatter;
|
import java.util.Formatter;
|
||||||
import java.util.List;
|
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.
|
* Validate if {@code value} is a valid csv field without double-quotes.
|
||||||
*
|
*
|
||||||
* @throws IllegalArgumentException if {@code value} needs to be encoded with double-quotes.
|
* @throws IllegalArgumentException if {@code value} needs to be encoded with double-quotes.
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package io.netty.util.internal;
|
package io.netty.util.internal;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import static io.netty.util.internal.StringUtil.*;
|
import static io.netty.util.internal.StringUtil.*;
|
||||||
|
@ -376,6 +377,47 @@ public class StringUtilTest {
|
||||||
assertEquals(value, unescapeCsv(StringUtil.escapeCsv(value)));
|
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
|
@Test
|
||||||
public void testSimpleClassName() throws Exception {
|
public void testSimpleClassName() throws Exception {
|
||||||
testSimpleClassName(String.class);
|
testSimpleClassName(String.class);
|
||||||
|
|
Loading…
Reference in New Issue
Block a user