Skip to content

Commit ecd3f19

Browse files
committed
Refactoring external contribution
Created abstract CharSequence decoder, which is extended by StringDecoder and CharBufferDecoder. See gh-29741
1 parent 5977131 commit ecd3f19

File tree

4 files changed

+276
-372
lines changed

4 files changed

+276
-372
lines changed
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.core.codec;
18+
19+
import java.nio.charset.Charset;
20+
import java.nio.charset.StandardCharsets;
21+
import java.util.ArrayList;
22+
import java.util.Collection;
23+
import java.util.Collections;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.concurrent.ConcurrentHashMap;
27+
import java.util.concurrent.ConcurrentMap;
28+
29+
import org.reactivestreams.Publisher;
30+
import reactor.core.publisher.Flux;
31+
import reactor.core.publisher.Mono;
32+
33+
import org.springframework.core.ResolvableType;
34+
import org.springframework.core.io.buffer.DataBuffer;
35+
import org.springframework.core.io.buffer.DataBufferUtils;
36+
import org.springframework.core.io.buffer.LimitedDataBufferList;
37+
import org.springframework.core.log.LogFormatUtils;
38+
import org.springframework.lang.Nullable;
39+
import org.springframework.util.Assert;
40+
import org.springframework.util.MimeType;
41+
42+
/**
43+
* Abstract base class that decodes from a data buffer stream to a
44+
* {@code CharSequence} stream.
45+
*
46+
* @author Arjen Poutsma
47+
* @since 6.1
48+
* @param <T> the character sequence type
49+
*/
50+
public abstract class AbstractCharSequenceDecoder<T extends CharSequence> extends AbstractDataBufferDecoder<T> {
51+
52+
/** The default charset to use, i.e. "UTF-8". */
53+
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
54+
55+
/** The default delimiter strings to use, i.e. {@code \r\n} and {@code \n}. */
56+
public static final List<String> DEFAULT_DELIMITERS = List.of("\r\n", "\n");
57+
58+
59+
private final List<String> delimiters;
60+
61+
private final boolean stripDelimiter;
62+
63+
private Charset defaultCharset = DEFAULT_CHARSET;
64+
65+
private final ConcurrentMap<Charset, byte[][]> delimitersCache = new ConcurrentHashMap<>();
66+
67+
68+
/**
69+
* Create a new {@code AbstractCharSequenceDecoder} with the given parameters.
70+
*/
71+
protected AbstractCharSequenceDecoder(List<String> delimiters, boolean stripDelimiter, MimeType... mimeTypes) {
72+
super(mimeTypes);
73+
Assert.notEmpty(delimiters, "'delimiters' must not be empty");
74+
this.delimiters = new ArrayList<>(delimiters);
75+
this.stripDelimiter = stripDelimiter;
76+
}
77+
78+
79+
/**
80+
* Set the default character set to fall back on if the MimeType does not specify any.
81+
* <p>By default this is {@code UTF-8}.
82+
* @param defaultCharset the charset to fall back on
83+
*/
84+
public void setDefaultCharset(Charset defaultCharset) {
85+
this.defaultCharset = defaultCharset;
86+
}
87+
88+
/**
89+
* Return the configured {@link #setDefaultCharset(Charset) defaultCharset}.
90+
*/
91+
public Charset getDefaultCharset() {
92+
return this.defaultCharset;
93+
}
94+
95+
96+
@Override
97+
public final Flux<T> decode(Publisher<DataBuffer> input, ResolvableType elementType,
98+
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
99+
100+
byte[][] delimiterBytes = getDelimiterBytes(mimeType);
101+
102+
LimitedDataBufferList chunks = new LimitedDataBufferList(getMaxInMemorySize());
103+
DataBufferUtils.Matcher matcher = DataBufferUtils.matcher(delimiterBytes);
104+
105+
return Flux.from(input)
106+
.concatMapIterable(buffer -> processDataBuffer(buffer, matcher, chunks))
107+
.concatWith(Mono.defer(() -> {
108+
if (chunks.isEmpty()) {
109+
return Mono.empty();
110+
}
111+
DataBuffer lastBuffer = chunks.get(0).factory().join(chunks);
112+
chunks.clear();
113+
return Mono.just(lastBuffer);
114+
}))
115+
.doFinally(signalType -> chunks.releaseAndClear())
116+
.doOnDiscard(DataBuffer.class, DataBufferUtils::release)
117+
.map(buffer -> decode(buffer, elementType, mimeType, hints));
118+
}
119+
120+
private byte[][] getDelimiterBytes(@Nullable MimeType mimeType) {
121+
return this.delimitersCache.computeIfAbsent(getCharset(mimeType), charset -> {
122+
byte[][] result = new byte[this.delimiters.size()][];
123+
for (int i = 0; i < this.delimiters.size(); i++) {
124+
result[i] = this.delimiters.get(i).getBytes(charset);
125+
}
126+
return result;
127+
});
128+
}
129+
130+
private Collection<DataBuffer> processDataBuffer(DataBuffer buffer, DataBufferUtils.Matcher matcher,
131+
LimitedDataBufferList chunks) {
132+
133+
boolean release = true;
134+
try {
135+
List<DataBuffer> result = null;
136+
do {
137+
int endIndex = matcher.match(buffer);
138+
if (endIndex == -1) {
139+
chunks.add(buffer);
140+
release = false;
141+
break;
142+
}
143+
DataBuffer split = buffer.split(endIndex + 1);
144+
if (result == null) {
145+
result = new ArrayList<>();
146+
}
147+
int delimiterLength = matcher.delimiter().length;
148+
if (chunks.isEmpty()) {
149+
if (this.stripDelimiter) {
150+
split.writePosition(split.writePosition() - delimiterLength);
151+
}
152+
result.add(split);
153+
}
154+
else {
155+
chunks.add(split);
156+
DataBuffer joined = buffer.factory().join(chunks);
157+
if (this.stripDelimiter) {
158+
joined.writePosition(joined.writePosition() - delimiterLength);
159+
}
160+
result.add(joined);
161+
chunks.clear();
162+
}
163+
}
164+
while (buffer.readableByteCount() > 0);
165+
return (result != null ? result : Collections.emptyList());
166+
}
167+
finally {
168+
if (release) {
169+
DataBufferUtils.release(buffer);
170+
}
171+
}
172+
}
173+
174+
@Override
175+
public final T decode(DataBuffer dataBuffer, ResolvableType elementType,
176+
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
177+
178+
Charset charset = getCharset(mimeType);
179+
T value = decodeInternal(dataBuffer, charset);
180+
DataBufferUtils.release(dataBuffer);
181+
LogFormatUtils.traceDebug(logger, traceOn -> {
182+
String formatted = LogFormatUtils.formatValue(value, !traceOn);
183+
return Hints.getLogPrefix(hints) + "Decoded " + formatted;
184+
});
185+
return value;
186+
}
187+
188+
private Charset getCharset(@Nullable MimeType mimeType) {
189+
if (mimeType != null) {
190+
Charset charset = mimeType.getCharset();
191+
if (charset != null) {
192+
return charset;
193+
}
194+
}
195+
return getDefaultCharset();
196+
}
197+
198+
199+
/**
200+
* Template method that decodes the given data buffer into {@code T}, given
201+
* the charset.
202+
*/
203+
protected abstract T decodeInternal(DataBuffer dataBuffer, Charset charset);
204+
205+
}

0 commit comments

Comments
 (0)