Suggestion for supporting single header fields.
Motivation: At the moment if you want to return a HTTP header containing multiple values you have to set/add that header once with the values wanted. If you used set/add with an array/iterable multiple HTTP header fields will be returned in the response. Note, that this is indeed a suggestion and additional work and tests should be added. This is mainly to bring up a discussion. Modifications: Added a flag to specify that when multiple values exist for a single HTTP header then add them as a comma separated string. In addition added a method to StringUtil to help escape comma separated value charsequences. Result: Allows for responses to be smaller.
This commit is contained in:
parent
99bd43ed51
commit
c53b8d5a85
@ -33,21 +33,31 @@ public class DefaultFullHttpResponse extends DefaultHttpResponse implements Full
|
||||
}
|
||||
|
||||
public DefaultFullHttpResponse(HttpVersion version, HttpResponseStatus status, ByteBuf content) {
|
||||
this(version, status, content, true);
|
||||
this(version, status, content, false);
|
||||
}
|
||||
|
||||
public DefaultFullHttpResponse(HttpVersion version, HttpResponseStatus status, boolean validateHeaders) {
|
||||
this(version, status, Unpooled.buffer(0), validateHeaders);
|
||||
this(version, status, Unpooled.buffer(0), validateHeaders, false);
|
||||
}
|
||||
|
||||
public DefaultFullHttpResponse(HttpVersion version, HttpResponseStatus status, boolean validateHeaders,
|
||||
boolean singleFieldHeaders) {
|
||||
this(version, status, Unpooled.buffer(0), validateHeaders, singleFieldHeaders);
|
||||
}
|
||||
|
||||
public DefaultFullHttpResponse(HttpVersion version, HttpResponseStatus status,
|
||||
ByteBuf content, boolean validateHeaders) {
|
||||
super(version, status, validateHeaders);
|
||||
ByteBuf content, boolean singleFieldHeaders) {
|
||||
this(version, status, content, true, singleFieldHeaders);
|
||||
}
|
||||
|
||||
public DefaultFullHttpResponse(HttpVersion version, HttpResponseStatus status,
|
||||
ByteBuf content, boolean validateHeaders, boolean singleFieldHeaders) {
|
||||
super(version, status, validateHeaders, singleFieldHeaders);
|
||||
if (content == null) {
|
||||
throw new NullPointerException("content");
|
||||
}
|
||||
this.content = content;
|
||||
trailingHeaders = new DefaultHttpHeaders(validateHeaders);
|
||||
trailingHeaders = new DefaultHttpHeaders(validateHeaders, singleFieldHeaders);
|
||||
this.validateHeaders = validateHeaders;
|
||||
}
|
||||
|
||||
|
@ -247,12 +247,17 @@ public class DefaultHttpHeaders extends HttpHeaders {
|
||||
}
|
||||
|
||||
public DefaultHttpHeaders(boolean validate) {
|
||||
this(true, validate? VALIDATE_NAME_CONVERTER : NO_VALIDATE_NAME_CONVERTER);
|
||||
this(true, validate? VALIDATE_NAME_CONVERTER : NO_VALIDATE_NAME_CONVERTER, false);
|
||||
}
|
||||
|
||||
protected DefaultHttpHeaders(boolean validate, NameConverter<CharSequence> nameConverter) {
|
||||
public DefaultHttpHeaders(boolean validate, boolean singleHeaderFields) {
|
||||
this(true, validate? VALIDATE_NAME_CONVERTER : NO_VALIDATE_NAME_CONVERTER, singleHeaderFields);
|
||||
}
|
||||
|
||||
protected DefaultHttpHeaders(boolean validate, NameConverter<CharSequence> nameConverter,
|
||||
boolean singleHeaderFields) {
|
||||
headers = new DefaultTextHeaders(true,
|
||||
validate ? VALIDATE_OBJECT_CONVERTER : NO_VALIDATE_OBJECT_CONVERTER, nameConverter);
|
||||
validate ? VALIDATE_OBJECT_CONVERTER : NO_VALIDATE_OBJECT_CONVERTER, nameConverter, singleHeaderFields);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -27,18 +27,18 @@ public abstract class DefaultHttpMessage extends DefaultHttpObject implements Ht
|
||||
* Creates a new instance.
|
||||
*/
|
||||
protected DefaultHttpMessage(final HttpVersion version) {
|
||||
this(version, true);
|
||||
this(version, true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*/
|
||||
protected DefaultHttpMessage(final HttpVersion version, boolean validateHeaders) {
|
||||
protected DefaultHttpMessage(final HttpVersion version, boolean validateHeaders, boolean singleHeaderFields) {
|
||||
if (version == null) {
|
||||
throw new NullPointerException("version");
|
||||
}
|
||||
this.version = version;
|
||||
headers = new DefaultHttpHeaders(validateHeaders);
|
||||
headers = new DefaultHttpHeaders(validateHeaders, singleHeaderFields);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -43,7 +43,7 @@ public class DefaultHttpRequest extends DefaultHttpMessage implements HttpReques
|
||||
* @param validateHeaders validate the header names and values when adding them to the {@link HttpHeaders}
|
||||
*/
|
||||
public DefaultHttpRequest(HttpVersion httpVersion, HttpMethod method, String uri, boolean validateHeaders) {
|
||||
super(httpVersion, validateHeaders);
|
||||
super(httpVersion, validateHeaders, false);
|
||||
if (method == null) {
|
||||
throw new NullPointerException("method");
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ public class DefaultHttpResponse extends DefaultHttpMessage implements HttpRespo
|
||||
* @param status the getStatus of this response
|
||||
*/
|
||||
public DefaultHttpResponse(HttpVersion version, HttpResponseStatus status) {
|
||||
this(version, status, true);
|
||||
this(version, status, true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -42,7 +42,21 @@ public class DefaultHttpResponse extends DefaultHttpMessage implements HttpRespo
|
||||
* @param validateHeaders validate the header names and values when adding them to the {@link HttpHeaders}
|
||||
*/
|
||||
public DefaultHttpResponse(HttpVersion version, HttpResponseStatus status, boolean validateHeaders) {
|
||||
super(version, validateHeaders);
|
||||
this(version, status, validateHeaders, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param version the HTTP version of this response
|
||||
* @param status the getStatus of this response
|
||||
* @param validateHeaders validate the header names and values when adding them to the {@link HttpHeaders}
|
||||
* @param singleHeaderFields determines if HTTP headers with multiple values should be added as a single
|
||||
* field or as multiple header fields.
|
||||
*/
|
||||
public DefaultHttpResponse(HttpVersion version, HttpResponseStatus status, boolean validateHeaders,
|
||||
boolean singleHeaderFields) {
|
||||
super(version, validateHeaders, singleHeaderFields);
|
||||
if (status == null) {
|
||||
throw new NullPointerException("status");
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ public class DefaultLastHttpContent extends DefaultHttpContent implements LastHt
|
||||
NO_VALIDATE_NAME_CONVERTER = new TrailingHttpHeadersNameConverter(false);
|
||||
|
||||
TrailingHttpHeaders(boolean validate) {
|
||||
super(validate, validate ? VALIDATE_NAME_CONVERTER : NO_VALIDATE_NAME_CONVERTER);
|
||||
super(validate, validate ? VALIDATE_NAME_CONVERTER : NO_VALIDATE_NAME_CONVERTER, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ public class CorsHandler extends ChannelDuplexHandler {
|
||||
}
|
||||
|
||||
private void handlePreflight(final ChannelHandlerContext ctx, final HttpRequest request) {
|
||||
final HttpResponse response = new DefaultFullHttpResponse(request.protocolVersion(), OK);
|
||||
final HttpResponse response = new DefaultFullHttpResponse(request.protocolVersion(), OK, true, true);
|
||||
if (setOrigin(response)) {
|
||||
setAllowMethods(response);
|
||||
setAllowHeaders(response);
|
||||
|
@ -81,7 +81,8 @@ public class CorsHandlerTest {
|
||||
.build();
|
||||
final HttpResponse response = preflightRequest(config, "http://localhost:8888", "content-type, xheader1");
|
||||
assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is("http://localhost:8888"));
|
||||
assertThat(response.headers().getAll(ACCESS_CONTROL_ALLOW_METHODS), hasItems("GET", "DELETE"));
|
||||
assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_METHODS), containsString("GET"));
|
||||
assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_METHODS), containsString("DELETE"));
|
||||
assertThat(response.headers().get(VARY), equalTo(ORIGIN.toString()));
|
||||
}
|
||||
|
||||
@ -93,8 +94,10 @@ public class CorsHandlerTest {
|
||||
.build();
|
||||
final HttpResponse response = preflightRequest(config, "http://localhost:8888", "content-type, xheader1");
|
||||
assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is("http://localhost:8888"));
|
||||
assertThat(response.headers().getAll(ACCESS_CONTROL_ALLOW_METHODS), hasItems("OPTIONS", "GET"));
|
||||
assertThat(response.headers().getAll(ACCESS_CONTROL_ALLOW_HEADERS), hasItems("content-type", "xheader1"));
|
||||
assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_METHODS), containsString("OPTIONS"));
|
||||
assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_METHODS), containsString("GET"));
|
||||
assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_HEADERS), containsString("content-type"));
|
||||
assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_HEADERS), containsString("xheader1"));
|
||||
assertThat(response.headers().get(VARY), equalTo(ORIGIN.toString()));
|
||||
}
|
||||
|
||||
@ -119,21 +122,27 @@ public class CorsHandlerTest {
|
||||
|
||||
@Test
|
||||
public void preflightRequestWithCustomHeaders() {
|
||||
final String headerName = "CustomHeader";
|
||||
final String value1 = "value1";
|
||||
final String value2 = "value2";
|
||||
final CorsConfig config = CorsConfig.withOrigin("http://localhost:8888")
|
||||
.preflightResponseHeader("CustomHeader", "value1", "value2")
|
||||
.preflightResponseHeader(headerName, value1, value2)
|
||||
.build();
|
||||
final HttpResponse response = preflightRequest(config, "http://localhost:8888", "content-type, xheader1");
|
||||
assertThat(response.headers().getAll("CustomHeader"), hasItems("value1", "value2"));
|
||||
assertValues(response, headerName, value1, value2);
|
||||
assertThat(response.headers().get(VARY), equalTo(ORIGIN.toString()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preflightRequestWithCustomHeadersIterable() {
|
||||
final String headerName = "CustomHeader";
|
||||
final String value1 = "value1";
|
||||
final String value2 = "value2";
|
||||
final CorsConfig config = CorsConfig.withOrigin("http://localhost:8888")
|
||||
.preflightResponseHeader("CustomHeader", Arrays.asList("value1", "value2"))
|
||||
.preflightResponseHeader(headerName, Arrays.asList(value1, value2))
|
||||
.build();
|
||||
final HttpResponse response = preflightRequest(config, "http://localhost:8888", "content-type, xheader1");
|
||||
assertThat(response.headers().getAll("CustomHeader"), hasItems("value1", "value2"));
|
||||
assertValues(response, headerName, value1, value2);
|
||||
assertThat(response.headers().get(VARY), equalTo(ORIGIN.toString()));
|
||||
}
|
||||
|
||||
@ -180,7 +189,8 @@ public class CorsHandlerTest {
|
||||
final CorsConfig config = CorsConfig.withAnyOrigin().exposeHeaders("custom1", "custom2").build();
|
||||
final HttpResponse response = simpleRequest(config, "http://localhost:7777");
|
||||
assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), equalTo("*"));
|
||||
assertThat(response.headers().getAll(ACCESS_CONTROL_EXPOSE_HEADERS), hasItems("custom1", "custom1"));
|
||||
assertThat(response.headers().get(ACCESS_CONTROL_EXPOSE_HEADERS), containsString("custom1"));
|
||||
assertThat(response.headers().get(ACCESS_CONTROL_EXPOSE_HEADERS), containsString("custom2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -210,7 +220,8 @@ public class CorsHandlerTest {
|
||||
public void simpleRequestExposeHeaders() {
|
||||
final CorsConfig config = CorsConfig.withAnyOrigin().exposeHeaders("one", "two").build();
|
||||
final HttpResponse response = simpleRequest(config, "http://localhost:7777");
|
||||
assertThat(response.headers().getAll(ACCESS_CONTROL_EXPOSE_HEADERS), hasItems("one", "two"));
|
||||
assertThat(response.headers().get(ACCESS_CONTROL_EXPOSE_HEADERS), containsString("one"));
|
||||
assertThat(response.headers().get(ACCESS_CONTROL_EXPOSE_HEADERS), containsString("two"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -306,7 +317,15 @@ public class CorsHandlerTest {
|
||||
private static class EchoHandler extends SimpleChannelInboundHandler<Object> {
|
||||
@Override
|
||||
public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
ctx.writeAndFlush(new DefaultFullHttpResponse(HTTP_1_1, OK));
|
||||
ctx.writeAndFlush(new DefaultFullHttpResponse(HTTP_1_1, OK, true, true));
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertValues(final HttpResponse response, final String headerName, final String... values) {
|
||||
final String header = response.headers().get(headerName);
|
||||
for (String value : values) {
|
||||
assertThat(header, containsString(value));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1216,6 +1216,10 @@ public class DefaultHeaders<T> implements Headers<T> {
|
||||
return builder.append(']').toString();
|
||||
}
|
||||
|
||||
protected ValueConverter<T> valueConverter() {
|
||||
return valueConverter;
|
||||
}
|
||||
|
||||
private T convertName(T name) {
|
||||
return nameConverter.convertName(checkNotNull(name, "name"));
|
||||
}
|
||||
|
@ -17,11 +17,14 @@
|
||||
package io.netty.handler.codec;
|
||||
|
||||
import io.netty.util.internal.PlatformDependent;
|
||||
import io.netty.util.internal.StringUtil;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
|
||||
import static io.netty.handler.codec.AsciiString.*;
|
||||
import static io.netty.util.internal.StringUtil.COMMA;
|
||||
|
||||
public class DefaultTextHeaders extends DefaultConvertibleHeaders<CharSequence, String> implements TextHeaders {
|
||||
private static final HashCodeGenerator<CharSequence> CHARSEQUECE_CASE_INSENSITIVE_HASH_CODE_GENERATOR =
|
||||
@ -145,10 +148,10 @@ public class DefaultTextHeaders extends DefaultConvertibleHeaders<CharSequence,
|
||||
}
|
||||
}
|
||||
|
||||
private static final Headers.ValueConverter<CharSequence> CHARSEQUENCE_FROM_OBJECT_CONVERTER =
|
||||
private static final ValueConverter<CharSequence> CHARSEQUENCE_FROM_OBJECT_CONVERTER =
|
||||
new DefaultTextValueTypeConverter();
|
||||
private static final ConvertibleHeaders.TypeConverter<CharSequence, String> CHARSEQUENCE_TO_STRING_CONVERTER =
|
||||
new ConvertibleHeaders.TypeConverter<CharSequence, String>() {
|
||||
private static final TypeConverter<CharSequence, String> CHARSEQUENCE_TO_STRING_CONVERTER =
|
||||
new TypeConverter<CharSequence, String>() {
|
||||
@Override
|
||||
public String toConvertedType(CharSequence value) {
|
||||
return value.toString();
|
||||
@ -162,6 +165,12 @@ public class DefaultTextHeaders extends DefaultConvertibleHeaders<CharSequence,
|
||||
|
||||
private static final NameConverter<CharSequence> CHARSEQUENCE_IDENTITY_CONVERTER =
|
||||
new IdentityNameConverter<CharSequence>();
|
||||
/**
|
||||
* An estimate of the size of a header value.
|
||||
*/
|
||||
private static final int DEFAULT_VALUE_SIZE = 10;
|
||||
|
||||
private final ValuesComposer valuesComposer;
|
||||
|
||||
public DefaultTextHeaders() {
|
||||
this(true);
|
||||
@ -171,12 +180,22 @@ public class DefaultTextHeaders extends DefaultConvertibleHeaders<CharSequence,
|
||||
this(ignoreCase, CHARSEQUENCE_FROM_OBJECT_CONVERTER, CHARSEQUENCE_IDENTITY_CONVERTER);
|
||||
}
|
||||
|
||||
public DefaultTextHeaders(boolean ignoreCase, Headers.ValueConverter<CharSequence> valueConverter,
|
||||
public DefaultTextHeaders(boolean ignoreCase, boolean singleHeaderFields) {
|
||||
this(ignoreCase, CHARSEQUENCE_FROM_OBJECT_CONVERTER, CHARSEQUENCE_IDENTITY_CONVERTER, singleHeaderFields);
|
||||
}
|
||||
|
||||
public DefaultTextHeaders(boolean ignoreCase, ValueConverter<CharSequence> valueConverter,
|
||||
NameConverter<CharSequence> nameConverter) {
|
||||
this(ignoreCase, valueConverter, nameConverter, false);
|
||||
}
|
||||
|
||||
public DefaultTextHeaders(boolean ignoreCase, ValueConverter<CharSequence> valueConverter,
|
||||
NameConverter<CharSequence> nameConverter, boolean singleHeaderFields) {
|
||||
super(comparator(ignoreCase), comparator(ignoreCase),
|
||||
ignoreCase ? CHARSEQUECE_CASE_INSENSITIVE_HASH_CODE_GENERATOR
|
||||
: CHARSEQUECE_CASE_SENSITIVE_HASH_CODE_GENERATOR, valueConverter,
|
||||
CHARSEQUENCE_TO_STRING_CONVERTER, nameConverter);
|
||||
valuesComposer = singleHeaderFields ? new SingleHeaderValuesComposer() : new MultipleFieldsValueComposer();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -191,38 +210,32 @@ public class DefaultTextHeaders extends DefaultConvertibleHeaders<CharSequence,
|
||||
|
||||
@Override
|
||||
public TextHeaders add(CharSequence name, CharSequence value) {
|
||||
super.add(name, value);
|
||||
return this;
|
||||
return valuesComposer.add(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders add(CharSequence name, Iterable<? extends CharSequence> values) {
|
||||
super.add(name, values);
|
||||
return this;
|
||||
return valuesComposer.add(name, values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders add(CharSequence name, CharSequence... values) {
|
||||
super.add(name, values);
|
||||
return this;
|
||||
return valuesComposer.add(name, values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders addObject(CharSequence name, Object value) {
|
||||
super.addObject(name, value);
|
||||
return this;
|
||||
return valuesComposer.addObject(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders addObject(CharSequence name, Iterable<?> values) {
|
||||
super.addObject(name, values);
|
||||
return this;
|
||||
return valuesComposer.addObject(name, values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders addObject(CharSequence name, Object... values) {
|
||||
super.addObject(name, values);
|
||||
return this;
|
||||
return valuesComposer.addObject(name, values);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -293,14 +306,12 @@ public class DefaultTextHeaders extends DefaultConvertibleHeaders<CharSequence,
|
||||
|
||||
@Override
|
||||
public TextHeaders set(CharSequence name, Iterable<? extends CharSequence> values) {
|
||||
super.set(name, values);
|
||||
return this;
|
||||
return valuesComposer.set(name, values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders set(CharSequence name, CharSequence... values) {
|
||||
super.set(name, values);
|
||||
return this;
|
||||
return valuesComposer.set(name, values);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -311,14 +322,12 @@ public class DefaultTextHeaders extends DefaultConvertibleHeaders<CharSequence,
|
||||
|
||||
@Override
|
||||
public TextHeaders setObject(CharSequence name, Iterable<?> values) {
|
||||
super.setObject(name, values);
|
||||
return this;
|
||||
return valuesComposer.setObject(name, values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders setObject(CharSequence name, Object... values) {
|
||||
super.setObject(name, values);
|
||||
return this;
|
||||
return valuesComposer.setObject(name, values);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -396,4 +405,228 @@ public class DefaultTextHeaders extends DefaultConvertibleHeaders<CharSequence,
|
||||
private static Comparator<CharSequence> comparator(boolean ignoreCase) {
|
||||
return ignoreCase ? CHARSEQUENCE_CASE_INSENSITIVE_ORDER : CHARSEQUENCE_CASE_SENSITIVE_ORDER;
|
||||
}
|
||||
|
||||
/*
|
||||
* This interface enables different implementations for adding/setting header values.
|
||||
* Concrete implementations can control how values are added, for example to add all
|
||||
* values for a header as a comma separated string instead of adding them as multiple
|
||||
* headers with a single value.
|
||||
*/
|
||||
private interface ValuesComposer {
|
||||
TextHeaders add(CharSequence name, CharSequence value);
|
||||
TextHeaders add(CharSequence name, CharSequence... values);
|
||||
TextHeaders add(CharSequence name, Iterable<? extends CharSequence> values);
|
||||
|
||||
TextHeaders addObject(CharSequence name, Iterable<?> values);
|
||||
TextHeaders addObject(CharSequence name, Object... values);
|
||||
|
||||
TextHeaders set(CharSequence name, CharSequence... values);
|
||||
TextHeaders set(CharSequence name, Iterable<? extends CharSequence> values);
|
||||
|
||||
TextHeaders setObject(CharSequence name, Object... values);
|
||||
TextHeaders setObject(CharSequence name, Iterable<?> values);
|
||||
}
|
||||
|
||||
/*
|
||||
* Will add multiple values for the same header as multiple separate headers.
|
||||
*/
|
||||
private final class MultipleFieldsValueComposer implements ValuesComposer {
|
||||
|
||||
@Override
|
||||
public TextHeaders add(CharSequence name, CharSequence value) {
|
||||
DefaultTextHeaders.super.add(name, value);
|
||||
return DefaultTextHeaders.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders add(CharSequence name, CharSequence... values) {
|
||||
DefaultTextHeaders.super.add(name, values);
|
||||
return DefaultTextHeaders.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders add(CharSequence name, Iterable<? extends CharSequence> values) {
|
||||
DefaultTextHeaders.super.add(name, values);
|
||||
return DefaultTextHeaders.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders addObject(CharSequence name, Iterable<?> values) {
|
||||
DefaultTextHeaders.super.addObject(name, values);
|
||||
return DefaultTextHeaders.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders addObject(CharSequence name, Object... values) {
|
||||
DefaultTextHeaders.super.addObject(name, values);
|
||||
return DefaultTextHeaders.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders set(CharSequence name, CharSequence... values) {
|
||||
DefaultTextHeaders.super.set(name, values);
|
||||
return DefaultTextHeaders.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders set(CharSequence name, Iterable<? extends CharSequence> values) {
|
||||
DefaultTextHeaders.super.set(name, values);
|
||||
return DefaultTextHeaders.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders setObject(CharSequence name, Object... values) {
|
||||
DefaultTextHeaders.super.setObject(name, values);
|
||||
return DefaultTextHeaders.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders setObject(CharSequence name, Iterable<?> values) {
|
||||
DefaultTextHeaders.super.setObject(name, values);
|
||||
return DefaultTextHeaders.this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will add multiple values for the same header as single header with a comma separated list of values.
|
||||
*
|
||||
* Please refer to section <a href="https://tools.ietf.org/html/rfc7230#section-3.2.2">3.2.2 Field Order</a>
|
||||
* of RFC-7230 for details.
|
||||
*/
|
||||
private final class SingleHeaderValuesComposer implements ValuesComposer {
|
||||
|
||||
private final ValueConverter<CharSequence> valueConverter = valueConverter();
|
||||
private CsvValueEscaper<Object> objectEscaper;
|
||||
private CsvValueEscaper<CharSequence> charSequenceEscaper;
|
||||
|
||||
private CsvValueEscaper<Object> objectEscaper() {
|
||||
if (objectEscaper == null) {
|
||||
objectEscaper = new CsvValueEscaper<Object>() {
|
||||
@Override
|
||||
public CharSequence escape(Object value) {
|
||||
return StringUtil.escapeCsv(valueConverter.convertObject(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
return objectEscaper;
|
||||
}
|
||||
|
||||
private CsvValueEscaper<CharSequence> charSequenceEscaper() {
|
||||
if (charSequenceEscaper == null) {
|
||||
charSequenceEscaper = new CsvValueEscaper<CharSequence>() {
|
||||
@Override
|
||||
public CharSequence escape(CharSequence value) {
|
||||
return StringUtil.escapeCsv(value);
|
||||
}
|
||||
};
|
||||
}
|
||||
return charSequenceEscaper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders add(CharSequence name, CharSequence value) {
|
||||
return addEscapedValue(name, StringUtil.escapeCsv(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders add(CharSequence name, CharSequence... values) {
|
||||
return addEscapedValue(name, commaSeparate(charSequenceEscaper(), values));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders add(CharSequence name, Iterable<? extends CharSequence> values) {
|
||||
return addEscapedValue(name, commaSeparate(charSequenceEscaper(), values));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders addObject(CharSequence name, Iterable<?> values) {
|
||||
return addEscapedValue(name, commaSeparate(objectEscaper(), values));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders addObject(CharSequence name, Object... values) {
|
||||
return addEscapedValue(name, commaSeparate(objectEscaper(), values));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders set(CharSequence name, CharSequence... values) {
|
||||
DefaultTextHeaders.super.set(name, commaSeparate(charSequenceEscaper(), values));
|
||||
return DefaultTextHeaders.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders set(CharSequence name, Iterable<? extends CharSequence> values) {
|
||||
DefaultTextHeaders.super.set(name, commaSeparate(charSequenceEscaper(), values));
|
||||
return DefaultTextHeaders.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders setObject(CharSequence name, Object... values) {
|
||||
DefaultTextHeaders.super.set(name, commaSeparate(objectEscaper(), values));
|
||||
return DefaultTextHeaders.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextHeaders setObject(CharSequence name, Iterable<?> values) {
|
||||
DefaultTextHeaders.super.set(name, commaSeparate(objectEscaper(), values));
|
||||
return DefaultTextHeaders.this;
|
||||
}
|
||||
|
||||
private TextHeaders addEscapedValue(CharSequence name, CharSequence escapedValue) {
|
||||
CharSequence currentValue = DefaultTextHeaders.super.get(name);
|
||||
if (currentValue == null) {
|
||||
DefaultTextHeaders.super.add(name, escapedValue);
|
||||
} else {
|
||||
DefaultTextHeaders.super.set(name, commaSeparateEscapedValues(currentValue, escapedValue));
|
||||
}
|
||||
return DefaultTextHeaders.this;
|
||||
}
|
||||
|
||||
private <T> CharSequence commaSeparate(CsvValueEscaper<T> escaper, T... values) {
|
||||
StringBuilder sb = new StringBuilder(values.length * DEFAULT_VALUE_SIZE);
|
||||
if (values.length > 0) {
|
||||
int end = values.length - 1;
|
||||
for (int i = 0; i < end; i++) {
|
||||
sb.append(escaper.escape(values[i])).append(COMMA);
|
||||
}
|
||||
sb.append(escaper.escape(values[end]));
|
||||
}
|
||||
return sb;
|
||||
}
|
||||
|
||||
private <T> CharSequence commaSeparate(CsvValueEscaper<T> escaper, Iterable<? extends T> values) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
Iterator<? extends T> iterator = values.iterator();
|
||||
if (iterator.hasNext()) {
|
||||
T next = iterator.next();
|
||||
while (iterator.hasNext()) {
|
||||
sb.append(escaper.escape(next)).append(COMMA);
|
||||
next = iterator.next();
|
||||
}
|
||||
sb.append(escaper.escape(next));
|
||||
}
|
||||
return sb;
|
||||
}
|
||||
|
||||
private CharSequence commaSeparateEscapedValues(CharSequence currentValue, CharSequence value) {
|
||||
return new StringBuilder(currentValue.length() + 1 + value.length())
|
||||
.append(currentValue)
|
||||
.append(COMMA)
|
||||
.append(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes comma separated values (CSV).
|
||||
*
|
||||
* @param <T> The type that a concrete implementation handles
|
||||
*/
|
||||
private interface CsvValueEscaper<T> {
|
||||
/**
|
||||
* Appends the value to the specified {@link StringBuilder}, escaping if necessary.
|
||||
*
|
||||
* @param value the value to be appended, escaped if necessary
|
||||
*/
|
||||
CharSequence escape(T value);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,360 @@
|
||||
/*
|
||||
* Copyright 2014 The Netty Project
|
||||
*
|
||||
* The Netty Project licenses this file to you under the Apache License, version
|
||||
* 2.0 (the "License"); you may not use this file except in compliance with the
|
||||
* License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package io.netty.handler.codec;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static io.netty.util.internal.StringUtil.COMMA;
|
||||
import static io.netty.util.internal.StringUtil.DOUBLE_QUOTE;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class DefaultTextHeadersTest {
|
||||
|
||||
private static final String HEADER_NAME = "testHeader";
|
||||
|
||||
@Test
|
||||
public void addCharSequences() {
|
||||
final TextHeaders headers = newDefaultTextHeaders();
|
||||
headers.add(HEADER_NAME, HeaderValue.THREE.asArray());
|
||||
assertDefaultValues(headers, HeaderValue.THREE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addCharSequencesCsv() {
|
||||
final TextHeaders headers = newCsvTextHeaders();
|
||||
headers.add(HEADER_NAME, HeaderValue.THREE.asArray());
|
||||
assertCsvValues(headers, HeaderValue.THREE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addCharSequencesCsvWithExistingHeader() {
|
||||
final TextHeaders headers = newCsvTextHeaders();
|
||||
headers.add(HEADER_NAME, HeaderValue.THREE.asArray());
|
||||
headers.add(HEADER_NAME, HeaderValue.FIVE.subset(4));
|
||||
assertCsvValues(headers, HeaderValue.FIVE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addCharSequencesCsvWithValueContainingComma() {
|
||||
final TextHeaders headers = newCsvTextHeaders();
|
||||
headers.add(HEADER_NAME, HeaderValue.SIX_QUOTED.subset(4));
|
||||
assertEquals(HeaderValue.SIX_QUOTED.subsetAsCsvString(4), headers.getAndConvert(HEADER_NAME));
|
||||
assertEquals(HeaderValue.SIX_QUOTED.subsetAsCsvString(4), headers.getAllAndConvert(HEADER_NAME).get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addCharSequencesCsvWithValueContainingCommas() {
|
||||
final TextHeaders headers = newCsvTextHeaders();
|
||||
headers.add(HEADER_NAME, HeaderValue.EIGHT.subset(6));
|
||||
assertEquals(HeaderValue.EIGHT.subsetAsCsvString(6), headers.getAndConvert(HEADER_NAME));
|
||||
assertEquals(HeaderValue.EIGHT.subsetAsCsvString(6), headers.getAllAndConvert(HEADER_NAME).get(0));
|
||||
}
|
||||
|
||||
@Test (expected = NullPointerException.class)
|
||||
public void addCharSequencesCsvNullValue() {
|
||||
final TextHeaders headers = newCsvTextHeaders();
|
||||
final String value = null;
|
||||
headers.add(HEADER_NAME, value);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addCharSequencesCsvMultipleTimes() {
|
||||
final TextHeaders headers = newCsvTextHeaders();
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
headers.add(HEADER_NAME, "value");
|
||||
}
|
||||
assertEquals("value,value,value,value,value", headers.getAndConvert(HEADER_NAME));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addCharSequenceCsv() {
|
||||
final TextHeaders headers = newCsvTextHeaders();
|
||||
addValues(headers, HeaderValue.ONE, HeaderValue.TWO, HeaderValue.THREE);
|
||||
assertCsvValues(headers, HeaderValue.THREE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addCharSequenceCsvSingleValue() {
|
||||
final TextHeaders headers = newCsvTextHeaders();
|
||||
addValues(headers, HeaderValue.ONE);
|
||||
assertCsvValue(headers, HeaderValue.ONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addIterable() {
|
||||
final TextHeaders headers = newDefaultTextHeaders();
|
||||
headers.add(HEADER_NAME, HeaderValue.THREE.asList());
|
||||
assertDefaultValues(headers, HeaderValue.THREE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addIterableCsv() {
|
||||
final TextHeaders headers = newCsvTextHeaders();
|
||||
headers.add(HEADER_NAME, HeaderValue.THREE.asList());
|
||||
assertCsvValues(headers, HeaderValue.THREE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addIterableCsvWithExistingHeader() {
|
||||
final TextHeaders headers = newCsvTextHeaders();
|
||||
headers.add(HEADER_NAME, HeaderValue.THREE.asArray());
|
||||
headers.add(HEADER_NAME, HeaderValue.FIVE.subset(4));
|
||||
assertCsvValues(headers, HeaderValue.FIVE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addIterableCsvSingleValue() {
|
||||
final TextHeaders headers = newCsvTextHeaders();
|
||||
headers.add(HEADER_NAME, HeaderValue.ONE.asList());
|
||||
assertCsvValue(headers, HeaderValue.ONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addIterableCsvEmtpy() {
|
||||
final TextHeaders headers = newCsvTextHeaders();
|
||||
headers.add(HEADER_NAME, Collections.<CharSequence>emptyList());
|
||||
assertEquals("", headers.getAllAndConvert(HEADER_NAME).get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addObjectCsv() {
|
||||
final TextHeaders headers = newCsvTextHeaders();
|
||||
addObjectValues(headers, HeaderValue.ONE, HeaderValue.TWO, HeaderValue.THREE);
|
||||
assertCsvValues(headers, HeaderValue.THREE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addObjects() {
|
||||
final TextHeaders headers = newDefaultTextHeaders();
|
||||
headers.addObject(HEADER_NAME, HeaderValue.THREE.asArray());
|
||||
assertDefaultValues(headers, HeaderValue.THREE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addObjectsCsv() {
|
||||
final TextHeaders headers = newCsvTextHeaders();
|
||||
headers.addObject(HEADER_NAME, HeaderValue.THREE.asArray());
|
||||
assertCsvValues(headers, HeaderValue.THREE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addObjectsIterableCsv() {
|
||||
final TextHeaders headers = newCsvTextHeaders();
|
||||
headers.addObject(HEADER_NAME, HeaderValue.THREE.asList());
|
||||
assertCsvValues(headers, HeaderValue.THREE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addObjectsCsvWithExistingHeader() {
|
||||
final TextHeaders headers = newCsvTextHeaders();
|
||||
headers.addObject(HEADER_NAME, HeaderValue.THREE.asArray());
|
||||
headers.addObject(HEADER_NAME, HeaderValue.FIVE.subset(4));
|
||||
assertCsvValues(headers, HeaderValue.FIVE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setCharSequences() {
|
||||
final TextHeaders headers = newDefaultTextHeaders();
|
||||
headers.set(HEADER_NAME, HeaderValue.THREE.asArray());
|
||||
assertDefaultValues(headers, HeaderValue.THREE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setCharSequenceCsv() {
|
||||
final TextHeaders headers = newCsvTextHeaders();
|
||||
headers.set(HEADER_NAME, HeaderValue.THREE.asArray());
|
||||
assertCsvValues(headers, HeaderValue.THREE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setIterable() {
|
||||
final TextHeaders headers = newDefaultTextHeaders();
|
||||
headers.set(HEADER_NAME, HeaderValue.THREE.asList());
|
||||
assertDefaultValues(headers, HeaderValue.THREE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setIterableCsv() {
|
||||
final TextHeaders headers = newCsvTextHeaders();
|
||||
headers.set(HEADER_NAME, HeaderValue.THREE.asList());
|
||||
assertCsvValues(headers, HeaderValue.THREE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setObjectObjects() {
|
||||
final TextHeaders headers = newDefaultTextHeaders();
|
||||
headers.setObject(HEADER_NAME, HeaderValue.THREE.asArray());
|
||||
assertDefaultValues(headers, HeaderValue.THREE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setObjectObjectsCsv() {
|
||||
final TextHeaders headers = newCsvTextHeaders();
|
||||
headers.setObject(HEADER_NAME, HeaderValue.THREE.asArray());
|
||||
assertCsvValues(headers, HeaderValue.THREE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setObjectIterable() {
|
||||
final TextHeaders headers = newDefaultTextHeaders();
|
||||
headers.setObject(HEADER_NAME, HeaderValue.THREE.asList());
|
||||
assertDefaultValues(headers, HeaderValue.THREE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setObjectIterableCsv() {
|
||||
final TextHeaders headers = newCsvTextHeaders();
|
||||
headers.setObject(HEADER_NAME, HeaderValue.THREE.asList());
|
||||
assertCsvValues(headers, HeaderValue.THREE);
|
||||
}
|
||||
|
||||
private static void assertDefaultValues(final TextHeaders headers, final HeaderValue headerValue) {
|
||||
assertEquals(headerValue.asArray()[0], headers.get(HEADER_NAME));
|
||||
assertEquals(headerValue.asList(), headers.getAll(HEADER_NAME));
|
||||
}
|
||||
|
||||
private static void assertCsvValues(final TextHeaders headers, final HeaderValue headerValue) {
|
||||
assertEquals(headerValue.asCsv(), headers.getAndConvert(HEADER_NAME));
|
||||
assertEquals(headerValue.asCsv(), headers.getAllAndConvert(HEADER_NAME).get(0));
|
||||
}
|
||||
|
||||
private static void assertCsvValue(final TextHeaders headers, final HeaderValue headerValue) {
|
||||
assertEquals(headerValue.toString(), headers.getAndConvert(HEADER_NAME));
|
||||
assertEquals(headerValue.toString(), headers.getAllAndConvert(HEADER_NAME).get(0));
|
||||
}
|
||||
|
||||
private static TextHeaders newDefaultTextHeaders() {
|
||||
return new DefaultTextHeaders();
|
||||
}
|
||||
|
||||
private static TextHeaders newCsvTextHeaders() {
|
||||
return new DefaultTextHeaders(true, true);
|
||||
}
|
||||
|
||||
private static void addValues(final TextHeaders headers, HeaderValue... headerValues) {
|
||||
for (HeaderValue v: headerValues) {
|
||||
headers.add(HEADER_NAME, v.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static void addObjectValues(final TextHeaders headers, HeaderValue... headerValues) {
|
||||
for (HeaderValue v: headerValues) {
|
||||
headers.addObject(HEADER_NAME, v.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private enum HeaderValue {
|
||||
UNKNOWN("unknown", 0),
|
||||
ONE("one", 1),
|
||||
TWO("two", 2),
|
||||
THREE("three", 3),
|
||||
FOUR("four", 4),
|
||||
FIVE("five", 5),
|
||||
SIX_QUOTED("six,", 6),
|
||||
SEVEN_QUOTED("seven; , GMT", 7),
|
||||
EIGHT("eight", 8);
|
||||
|
||||
private final int nr;
|
||||
private final String value;
|
||||
private String[] array;
|
||||
private static final String DOUBLE_QUOTE_STRING = String.valueOf(DOUBLE_QUOTE);
|
||||
|
||||
HeaderValue(final String value, final int nr) {
|
||||
this.nr = nr;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public String[] asArray() {
|
||||
if (array == null) {
|
||||
final String[] arr = new String[nr];
|
||||
for (int i = 1, y = 0; i <= nr; i++, y++) {
|
||||
arr[y] = of(i).toString();
|
||||
}
|
||||
array = arr;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
public String[] subset(final int from) {
|
||||
final int size = from - 1;
|
||||
final String[] arr = new String[nr - size];
|
||||
System.arraycopy(asArray(), size, arr, 0, arr.length);
|
||||
return arr;
|
||||
}
|
||||
|
||||
public String subsetAsCsvString(final int from) {
|
||||
final String[] subset = subset(from);
|
||||
return asCsv(subset);
|
||||
}
|
||||
|
||||
public List<CharSequence> asList() {
|
||||
return Arrays.<CharSequence>asList(asArray());
|
||||
}
|
||||
|
||||
public String asCsv(final String[] arr) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
int end = arr.length - 1;
|
||||
for (int i = 0; i < end; i++) {
|
||||
final String value = arr[i];
|
||||
quoted(sb, value).append(COMMA);
|
||||
}
|
||||
quoted(sb, arr[end]);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public String asCsv() {
|
||||
return asCsv(asArray());
|
||||
}
|
||||
|
||||
private static StringBuilder quoted(final StringBuilder sb, final String value) {
|
||||
if (value.contains(String.valueOf(COMMA)) && !value.contains(DOUBLE_QUOTE_STRING)) {
|
||||
return sb.append(DOUBLE_QUOTE).append(value).append(DOUBLE_QUOTE);
|
||||
}
|
||||
return sb.append(value);
|
||||
}
|
||||
|
||||
public static String quoted(final String value) {
|
||||
return quoted(new StringBuilder(), value).toString();
|
||||
}
|
||||
|
||||
private static final Map<Integer, HeaderValue> MAP;
|
||||
|
||||
static {
|
||||
final Map<Integer, HeaderValue> map = new HashMap<Integer, HeaderValue>();
|
||||
for (HeaderValue v : values()) {
|
||||
final int nr = v.nr;
|
||||
map.put(Integer.valueOf(nr), v);
|
||||
}
|
||||
MAP = map;
|
||||
}
|
||||
|
||||
public static HeaderValue of(final int nr) {
|
||||
final HeaderValue v = MAP.get(Integer.valueOf(nr));
|
||||
return v == null ? UNKNOWN : v;
|
||||
}
|
||||
}
|
||||
}
|
@ -15,21 +15,32 @@
|
||||
*/
|
||||
package io.netty.util.internal;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Formatter;
|
||||
import java.util.List;
|
||||
|
||||
import static io.netty.util.internal.ObjectUtil.checkNotNull;
|
||||
|
||||
/**
|
||||
* String utility class.
|
||||
*/
|
||||
public final class StringUtil {
|
||||
|
||||
public static final String NEWLINE;
|
||||
|
||||
public static final char DOUBLE_QUOTE = '\"';
|
||||
public static final char COMMA = ',';
|
||||
public static final char LINE_FEED = '\n';
|
||||
public static final char CARRIAGE_RETURN = '\r';
|
||||
private static final String[] BYTE2HEX_PAD = new String[256];
|
||||
private static final String[] BYTE2HEX_NOPAD = new String[256];
|
||||
private static final String EMPTY_STRING = "";
|
||||
/**
|
||||
* 2 - Quote character at beginning and end.
|
||||
* 5 - Extra allowance for anticipated escape characters that may be added.
|
||||
*/
|
||||
private static final int CSV_NUMBER_ESCAPE_CHARACTERS = 2 + 5;
|
||||
|
||||
static {
|
||||
// Determine the newline character of the current platform.
|
||||
@ -314,6 +325,58 @@ public final class StringUtil {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes the specified value, if necessary according to
|
||||
* <a href="https://tools.ietf.org/html/rfc4180#section-2">RFC-4180</a>.
|
||||
*
|
||||
* @param value The value which will be escaped according to
|
||||
* <a href="https://tools.ietf.org/html/rfc4180#section-2">RFC-4180</a>
|
||||
* @return {@link CharSequence} the escaped value if nesessary, or the value unchanged
|
||||
*/
|
||||
public static CharSequence escapeCsv(CharSequence value) {
|
||||
int length = checkNotNull(value, "value").length();
|
||||
if (length == 0) {
|
||||
return value;
|
||||
}
|
||||
int last = length - 1;
|
||||
boolean quoted = isDoubleQuote(value.charAt(0)) && isDoubleQuote(value.charAt(last)) && length != 1;
|
||||
boolean foundSpecialCharacter = false;
|
||||
boolean escapedDoubleQuote = false;
|
||||
StringBuilder escaped = new StringBuilder(length + CSV_NUMBER_ESCAPE_CHARACTERS).append(DOUBLE_QUOTE);
|
||||
for (int i = 0; i < length; i++) {
|
||||
char current = value.charAt(i);
|
||||
switch (current) {
|
||||
case DOUBLE_QUOTE:
|
||||
if (i == 0 || i == last) {
|
||||
if (!quoted) {
|
||||
escaped.append(DOUBLE_QUOTE);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
boolean isNextCharDoubleQuote = isDoubleQuote(value.charAt(i + 1));
|
||||
if (!isDoubleQuote(value.charAt(i - 1)) &&
|
||||
(!isNextCharDoubleQuote || isNextCharDoubleQuote && i + 1 == last)) {
|
||||
escaped.append(DOUBLE_QUOTE);
|
||||
escapedDoubleQuote = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LINE_FEED:
|
||||
case CARRIAGE_RETURN:
|
||||
case COMMA:
|
||||
foundSpecialCharacter = true;
|
||||
}
|
||||
escaped.append(current);
|
||||
}
|
||||
return escapedDoubleQuote || foundSpecialCharacter && !quoted ?
|
||||
escaped.append(DOUBLE_QUOTE) : value;
|
||||
}
|
||||
|
||||
private static boolean isDoubleQuote(char c) {
|
||||
return c == DOUBLE_QUOTE;
|
||||
}
|
||||
|
||||
private StringUtil() {
|
||||
// Unused.
|
||||
}
|
||||
|
@ -81,4 +81,200 @@ public class StringUtilTest {
|
||||
public void substringAfterTest() {
|
||||
assertEquals("bar:bar2", substringAfter("foo:bar:bar2", ':'));
|
||||
}
|
||||
|
||||
@Test (expected = NullPointerException.class)
|
||||
public void escapeCsvNull() {
|
||||
StringUtil.escapeCsv(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvEmpty() {
|
||||
CharSequence value = "";
|
||||
CharSequence expected = value;
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvUnquoted() {
|
||||
CharSequence value = "something";
|
||||
CharSequence expected = value;
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvAlreadyQuoted() {
|
||||
CharSequence value = "\"something\"";
|
||||
CharSequence expected = "\"something\"";
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvWithQuote() {
|
||||
CharSequence value = "s\"";
|
||||
CharSequence expected = "\"s\"\"\"";
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvWithQuoteInMiddle() {
|
||||
CharSequence value = "some text\"and more text";
|
||||
CharSequence expected = "\"some text\"\"and more text\"";
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvWithQuoteInMiddleAlreadyQuoted() {
|
||||
CharSequence value = "\"some text\"and more text\"";
|
||||
CharSequence expected = "\"some text\"\"and more text\"";
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvWithQuotedWords() {
|
||||
CharSequence value = "\"foo\"\"goo\"";
|
||||
CharSequence expected = "\"foo\"\"goo\"";
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvWithAlreadyEscapedQuote() {
|
||||
CharSequence value = "foo\"\"goo";
|
||||
CharSequence expected = "foo\"\"goo";
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvEndingWithQuote() {
|
||||
CharSequence value = "some\"";
|
||||
CharSequence expected = "\"some\"\"\"";
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvWithSingleQuote() {
|
||||
CharSequence value = "\"";
|
||||
CharSequence expected = "\"\"\"\"";
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvWithSingleQuoteAndCharacter() {
|
||||
CharSequence value = "\"f";
|
||||
CharSequence expected = "\"\"\"f\"";
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvAlreadyEscapedQuote() {
|
||||
CharSequence value = "\"some\"\"";
|
||||
CharSequence expected = "\"some\"\"\"";
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvQuoted() {
|
||||
CharSequence value = "\"foo,goo\"";
|
||||
CharSequence expected = value;
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvWithLineFeed() {
|
||||
CharSequence value = "some text\n more text";
|
||||
CharSequence expected = "\"some text\n more text\"";
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvWithSingleLineFeedCharacter() {
|
||||
CharSequence value = "\n";
|
||||
CharSequence expected = "\"\n\"";
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvWithMultipleLineFeedCharacter() {
|
||||
CharSequence value = "\n\n";
|
||||
CharSequence expected = "\"\n\n\"";
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvWithQuotedAndLineFeedCharacter() {
|
||||
CharSequence value = " \" \n ";
|
||||
CharSequence expected = "\" \"\" \n \"";
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvWithLineFeedAtEnd() {
|
||||
CharSequence value = "testing\n";
|
||||
CharSequence expected = "\"testing\n\"";
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvWithComma() {
|
||||
CharSequence value = "test,ing";
|
||||
CharSequence expected = "\"test,ing\"";
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvWithSingleComma() {
|
||||
CharSequence value = ",";
|
||||
CharSequence expected = "\",\"";
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvWithSingleCarriageReturn() {
|
||||
CharSequence value = "\r";
|
||||
CharSequence expected = "\"\r\"";
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvWithMultipleCarriageReturn() {
|
||||
CharSequence value = "\r\r";
|
||||
CharSequence expected = "\"\r\r\"";
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvWithCarriageReturn() {
|
||||
CharSequence value = "some text\r more text";
|
||||
CharSequence expected = "\"some text\r more text\"";
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvWithQuotedAndCarriageReturnCharacter() {
|
||||
CharSequence value = "\"\r";
|
||||
CharSequence expected = "\"\"\"\r\"";
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvWithCarriageReturnAtEnd() {
|
||||
CharSequence value = "testing\r";
|
||||
CharSequence expected = "\"testing\r\"";
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapeCsvWithCRLFCharacter() {
|
||||
CharSequence value = "\r\n";
|
||||
CharSequence expected = "\"\r\n\"";
|
||||
escapeCsv(value, expected);
|
||||
}
|
||||
|
||||
private static void escapeCsv(CharSequence value, CharSequence expected) {
|
||||
CharSequence escapedValue = value;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
escapedValue = StringUtil.escapeCsv(escapedValue);
|
||||
assertEquals(expected, escapedValue.toString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user