Skip to content

Commit 4182935

Browse files
committed
Revert optimization in StringDecoder
This commit reverts the first optimizations listed in fa096dc, as the default delimiters do vary, namely by the charset given in the message mime type. The mimetype charset might not be compatible with ASCII (i.e. anything but UTF-8 or ISO-8859-1, for instance it might be UTF-16), and will not successfully find the default delimiters as a consequence. Added test to indicate the bug.
1 parent 445b76b commit 4182935

File tree

2 files changed

+54
-26
lines changed

2 files changed

+54
-26
lines changed

spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import java.util.Arrays;
2424
import java.util.List;
2525
import java.util.Map;
26+
import java.util.concurrent.ConcurrentHashMap;
27+
import java.util.concurrent.ConcurrentMap;
2628
import java.util.stream.Collectors;
2729

2830
import org.reactivestreams.Publisher;
@@ -35,6 +37,7 @@
3537
import org.springframework.core.io.buffer.PooledDataBuffer;
3638
import org.springframework.core.log.LogFormatUtils;
3739
import org.springframework.lang.Nullable;
40+
import org.springframework.util.Assert;
3841
import org.springframework.util.MimeType;
3942
import org.springframework.util.MimeTypeUtils;
4043

@@ -63,20 +66,18 @@ public final class StringDecoder extends AbstractDataBufferDecoder<String> {
6366
/** The default delimiter strings to use, i.e. {@code \r\n} and {@code \n}. */
6467
public static final List<String> DEFAULT_DELIMITERS = Arrays.asList("\r\n", "\n");
6568

66-
private static final List<byte[]> DEFAULT_DELIMITER_BYTES = DEFAULT_DELIMITERS.stream()
67-
.map(str -> str.getBytes(StandardCharsets.UTF_8))
68-
.collect(Collectors.toList());
6969

70-
71-
@Nullable
7270
private final List<String> delimiters;
7371

7472
private final boolean stripDelimiter;
7573

74+
private final ConcurrentMap<Charset, List<byte[]>> delimitersCache = new ConcurrentHashMap<>();
75+
7676

77-
private StringDecoder(@Nullable List<String> delimiters, boolean stripDelimiter, MimeType... mimeTypes) {
77+
private StringDecoder(List<String> delimiters, boolean stripDelimiter, MimeType... mimeTypes) {
7878
super(mimeTypes);
79-
this.delimiters = delimiters != null ? new ArrayList<>(delimiters) : null;
79+
Assert.notEmpty(delimiters, "'delimiters' must not be empty");
80+
this.delimiters = new ArrayList<>(delimiters);
8081
this.stripDelimiter = stripDelimiter;
8182
}
8283

@@ -90,9 +91,7 @@ public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType
9091
public Flux<String> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType,
9192
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
9293

93-
List<byte[]> delimiterBytes = this.delimiters != null ?
94-
this.delimiters.stream().map(s -> s.getBytes(getCharset(mimeType))).collect(Collectors.toList()) :
95-
DEFAULT_DELIMITER_BYTES;
94+
List<byte[]> delimiterBytes = getDelimiterBytes(mimeType);
9695

9796
Flux<DataBuffer> inputFlux = Flux.from(inputStream)
9897
.flatMapIterable(dataBuffer -> splitOnDelimiter(dataBuffer, delimiterBytes))
@@ -103,6 +102,13 @@ public Flux<String> decode(Publisher<DataBuffer> inputStream, ResolvableType ele
103102
return super.decode(inputFlux, elementType, mimeType, hints);
104103
}
105104

105+
private List<byte[]> getDelimiterBytes(@Nullable MimeType mimeType) {
106+
return this.delimitersCache.computeIfAbsent(getCharset(mimeType),
107+
charset -> this.delimiters.stream()
108+
.map(s -> s.getBytes(charset))
109+
.collect(Collectors.toList()));
110+
}
111+
106112
/**
107113
* Splits the given data buffer on delimiter boundaries. The returned Flux contains a
108114
* {@link #END_FRAME} buffer after each delimiter.
@@ -234,17 +240,16 @@ public static StringDecoder textPlainOnly(boolean ignored) {
234240
* Create a {@code StringDecoder} for {@code "text/plain"}.
235241
*/
236242
public static StringDecoder textPlainOnly() {
237-
return textPlainOnly(null, true);
243+
return textPlainOnly(DEFAULT_DELIMITERS, true);
238244
}
239245

240246
/**
241247
* Create a {@code StringDecoder} for {@code "text/plain"}.
242-
* @param delimiters delimiter strings to use to split the input stream, if
243-
* {@code null} by default {@link #DEFAULT_DELIMITERS} is used.
248+
* @param delimiters delimiter strings to use to split the input stream
244249
* @param stripDelimiter whether to remove delimiters from the resulting
245250
* input strings.
246251
*/
247-
public static StringDecoder textPlainOnly(@Nullable List<String> delimiters, boolean stripDelimiter) {
252+
public static StringDecoder textPlainOnly(List<String> delimiters, boolean stripDelimiter) {
248253
return new StringDecoder(delimiters, stripDelimiter, new MimeType("text", "plain", DEFAULT_CHARSET));
249254
}
250255

@@ -263,17 +268,16 @@ public static StringDecoder allMimeTypes(boolean ignored) {
263268
* Create a {@code StringDecoder} that supports all MIME types.
264269
*/
265270
public static StringDecoder allMimeTypes() {
266-
return allMimeTypes(null, true);
271+
return allMimeTypes(DEFAULT_DELIMITERS, true);
267272
}
268273

269274
/**
270275
* Create a {@code StringDecoder} that supports all MIME types.
271-
* @param delimiters delimiter strings to use to split the input stream, if
272-
* {@code null} by default {@link #DEFAULT_DELIMITERS} is used.
276+
* @param delimiters delimiter strings to use to split the input stream
273277
* @param stripDelimiter whether to remove delimiters from the resulting
274278
* input strings.
275279
*/
276-
public static StringDecoder allMimeTypes(@Nullable List<String> delimiters, boolean stripDelimiter) {
280+
public static StringDecoder allMimeTypes(List<String> delimiters, boolean stripDelimiter) {
277281
return new StringDecoder(delimiters, stripDelimiter,
278282
new MimeType("text", "plain", DEFAULT_CHARSET), MimeTypeUtils.ALL);
279283
}

spring-core/src/test/java/org/springframework/core/codec/StringDecoderTests.java

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
package org.springframework.core.codec;
1818

19-
import java.nio.charset.StandardCharsets;
19+
import java.nio.charset.Charset;
2020
import java.util.ArrayList;
2121
import java.util.Collections;
2222
import java.util.List;
@@ -29,8 +29,11 @@
2929
import org.springframework.core.ResolvableType;
3030
import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase;
3131
import org.springframework.core.io.buffer.DataBuffer;
32+
import org.springframework.util.MimeType;
3233
import org.springframework.util.MimeTypeUtils;
3334

35+
import static java.nio.charset.StandardCharsets.UTF_16BE;
36+
import static java.nio.charset.StandardCharsets.UTF_8;
3437
import static org.junit.Assert.*;
3538

3639
/**
@@ -69,22 +72,43 @@ public void canDecode() {
6972

7073
@Test
7174
public void decodeMultibyteCharacter() {
72-
String s = "üéø";
73-
Flux<DataBuffer> source = toSingleByteDataBuffers(s);
75+
String u = "ü";
76+
String e = "é";
77+
String o = "ø";
78+
String s = String.format("%s\n%s\n%s", u, e, o);
79+
Flux<DataBuffer> source = toDataBuffers(s, 1, UTF_8);
7480

7581
Flux<String> output = this.decoder.decode(source, ResolvableType.forClass(String.class),
7682
null, Collections.emptyMap());
7783
StepVerifier.create(output)
78-
.expectNext(s)
84+
.expectNext(u, e, o)
7985
.verifyComplete();
8086
}
8187

82-
private Flux<DataBuffer> toSingleByteDataBuffers(String s) {
83-
byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
88+
@Test
89+
public void decodeMultibyteCharacterUtf16() {
90+
String u = "ü";
91+
String e = "é";
92+
String o = "ø";
93+
String s = String.format("%s\n%s\n%s", u, e, o);
94+
Flux<DataBuffer> source = toDataBuffers(s, 2, UTF_16BE);
95+
96+
MimeType mimeType = MimeTypeUtils.parseMimeType("text/plain;charset=utf-16be");
97+
Flux<String> output = this.decoder.decode(source, ResolvableType.forClass(String.class),
98+
mimeType, Collections.emptyMap());
99+
StepVerifier.create(output)
100+
.expectNext(u, e, o)
101+
.verifyComplete();
102+
}
103+
104+
private Flux<DataBuffer> toDataBuffers(String s, int length, Charset charset) {
105+
byte[] bytes = s.getBytes(charset);
84106

85107
List<DataBuffer> dataBuffers = new ArrayList<>();
86-
for (byte b : bytes) {
87-
dataBuffers.add(this.bufferFactory.wrap(new byte[]{b}));
108+
for (int i = 0; i < bytes.length; i += length) {
109+
DataBuffer dataBuffer = this.bufferFactory.allocateBuffer(length);
110+
dataBuffer.write(bytes, i, length);
111+
dataBuffers.add(dataBuffer);
88112
}
89113
return Flux.fromIterable(dataBuffers);
90114
}

0 commit comments

Comments
 (0)