001/*
002 * Copyright 2002-2020 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.core.codec;
018
019import java.nio.CharBuffer;
020import java.nio.charset.Charset;
021import java.nio.charset.StandardCharsets;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.List;
027import java.util.Map;
028import java.util.concurrent.ConcurrentHashMap;
029import java.util.concurrent.ConcurrentMap;
030
031import org.reactivestreams.Publisher;
032import reactor.core.publisher.Flux;
033import reactor.core.publisher.Mono;
034
035import org.springframework.core.ResolvableType;
036import org.springframework.core.io.buffer.DataBuffer;
037import org.springframework.core.io.buffer.DataBufferUtils;
038import org.springframework.core.io.buffer.LimitedDataBufferList;
039import org.springframework.core.io.buffer.PooledDataBuffer;
040import org.springframework.core.log.LogFormatUtils;
041import org.springframework.lang.Nullable;
042import org.springframework.util.Assert;
043import org.springframework.util.MimeType;
044import org.springframework.util.MimeTypeUtils;
045
046/**
047 * Decode from a data buffer stream to a {@code String} stream, either splitting
048 * or aggregating incoming data chunks to realign along newlines delimiters
049 * and produce a stream of strings. This is useful for streaming but is also
050 * necessary to ensure that that multibyte characters can be decoded correctly,
051 * avoiding split-character issues. The default delimiters used by default are
052 * {@code \n} and {@code \r\n} but that can be customized.
053 *
054 * @author Sebastien Deleuze
055 * @author Brian Clozel
056 * @author Arjen Poutsma
057 * @author Mark Paluch
058 * @since 5.0
059 * @see CharSequenceEncoder
060 */
061public final class StringDecoder extends AbstractDataBufferDecoder<String> {
062
063        /** The default charset to use, i.e. "UTF-8". */
064        public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
065
066        /** The default delimiter strings to use, i.e. {@code \r\n} and {@code \n}. */
067        public static final List<String> DEFAULT_DELIMITERS = Arrays.asList("\r\n", "\n");
068
069
070        private final List<String> delimiters;
071
072        private final boolean stripDelimiter;
073
074        private Charset defaultCharset = DEFAULT_CHARSET;
075
076        private final ConcurrentMap<Charset, byte[][]> delimitersCache = new ConcurrentHashMap<>();
077
078
079        private StringDecoder(List<String> delimiters, boolean stripDelimiter, MimeType... mimeTypes) {
080                super(mimeTypes);
081                Assert.notEmpty(delimiters, "'delimiters' must not be empty");
082                this.delimiters = new ArrayList<>(delimiters);
083                this.stripDelimiter = stripDelimiter;
084        }
085
086
087        /**
088         * Set the default character set to fall back on if the MimeType does not specify any.
089         * <p>By default this is {@code UTF-8}.
090         * @param defaultCharset the charset to fall back on
091         * @since 5.2.9
092         */
093        public void setDefaultCharset(Charset defaultCharset) {
094                this.defaultCharset = defaultCharset;
095        }
096
097        /**
098         * Return the configured {@link #setDefaultCharset(Charset) defaultCharset}.
099         * @since 5.2.9
100         */
101        public Charset getDefaultCharset() {
102                return this.defaultCharset;
103        }
104
105
106        @Override
107        public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) {
108                return (elementType.resolve() == String.class && super.canDecode(elementType, mimeType));
109        }
110
111        @Override
112        public Flux<String> decode(Publisher<DataBuffer> input, ResolvableType elementType,
113                        @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
114
115                byte[][] delimiterBytes = getDelimiterBytes(mimeType);
116
117                LimitedDataBufferList chunks = new LimitedDataBufferList(getMaxInMemorySize());
118                DataBufferUtils.Matcher matcher = DataBufferUtils.matcher(delimiterBytes);
119
120                return Flux.from(input)
121                                .concatMapIterable(buffer -> processDataBuffer(buffer, matcher, chunks))
122                                .concatWith(Mono.defer(() -> {
123                                        if (chunks.isEmpty()) {
124                                                return Mono.empty();
125                                        }
126                                        DataBuffer lastBuffer = chunks.get(0).factory().join(chunks);
127                                        chunks.clear();
128                                        return Mono.just(lastBuffer);
129                                }))
130                                .doOnTerminate(chunks::releaseAndClear)
131                                .doOnDiscard(PooledDataBuffer.class, PooledDataBuffer::release)
132                                .map(buffer -> decode(buffer, elementType, mimeType, hints));
133        }
134
135        private byte[][] getDelimiterBytes(@Nullable MimeType mimeType) {
136                return this.delimitersCache.computeIfAbsent(getCharset(mimeType), charset -> {
137                        byte[][] result = new byte[this.delimiters.size()][];
138                        for (int i = 0; i < this.delimiters.size(); i++) {
139                                result[i] = this.delimiters.get(i).getBytes(charset);
140                        }
141                        return result;
142                });
143        }
144
145        private Collection<DataBuffer> processDataBuffer(
146                        DataBuffer buffer, DataBufferUtils.Matcher matcher, LimitedDataBufferList chunks) {
147
148                try {
149                        List<DataBuffer> result = null;
150                        do {
151                                int endIndex = matcher.match(buffer);
152                                if (endIndex == -1) {
153                                        chunks.add(buffer);
154                                        DataBufferUtils.retain(buffer); // retain after add (may raise DataBufferLimitException)
155                                        break;
156                                }
157                                int startIndex = buffer.readPosition();
158                                int length = (endIndex - startIndex + 1);
159                                DataBuffer slice = buffer.retainedSlice(startIndex, length);
160                                if (this.stripDelimiter) {
161                                        slice.writePosition(slice.writePosition() - matcher.delimiter().length);
162                                }
163                                result = (result != null ? result : new ArrayList<>());
164                                if (chunks.isEmpty()) {
165                                        result.add(slice);
166                                }
167                                else {
168                                        chunks.add(slice);
169                                        result.add(buffer.factory().join(chunks));
170                                        chunks.clear();
171                                }
172                                buffer.readPosition(endIndex + 1);
173                        }
174                        while (buffer.readableByteCount() > 0);
175                        return (result != null ? result : Collections.emptyList());
176                }
177                finally {
178                        DataBufferUtils.release(buffer);
179                }
180        }
181
182        @Override
183        public String decode(DataBuffer dataBuffer, ResolvableType elementType,
184                        @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
185
186                Charset charset = getCharset(mimeType);
187                CharBuffer charBuffer = charset.decode(dataBuffer.asByteBuffer());
188                DataBufferUtils.release(dataBuffer);
189                String value = charBuffer.toString();
190                LogFormatUtils.traceDebug(logger, traceOn -> {
191                        String formatted = LogFormatUtils.formatValue(value, !traceOn);
192                        return Hints.getLogPrefix(hints) + "Decoded " + formatted;
193                });
194                return value;
195        }
196
197        private Charset getCharset(@Nullable MimeType mimeType) {
198                if (mimeType != null && mimeType.getCharset() != null) {
199                        return mimeType.getCharset();
200                }
201                else {
202                        return getDefaultCharset();
203                }
204        }
205
206        /**
207         * Create a {@code StringDecoder} for {@code "text/plain"}.
208         * @param stripDelimiter this flag is ignored
209         * @deprecated as of Spring 5.0.4, in favor of {@link #textPlainOnly()} or
210         * {@link #textPlainOnly(List, boolean)}
211         */
212        @Deprecated
213        public static StringDecoder textPlainOnly(boolean stripDelimiter) {
214                return textPlainOnly();
215        }
216
217        /**
218         * Create a {@code StringDecoder} for {@code "text/plain"}.
219         */
220        public static StringDecoder textPlainOnly() {
221                return textPlainOnly(DEFAULT_DELIMITERS, true);
222        }
223
224        /**
225         * Create a {@code StringDecoder} for {@code "text/plain"}.
226         * @param delimiters delimiter strings to use to split the input stream
227         * @param stripDelimiter whether to remove delimiters from the resulting
228         * input strings
229         */
230        public static StringDecoder textPlainOnly(List<String> delimiters, boolean stripDelimiter) {
231                return new StringDecoder(delimiters, stripDelimiter, new MimeType("text", "plain", DEFAULT_CHARSET));
232        }
233
234        /**
235         * Create a {@code StringDecoder} that supports all MIME types.
236         * @param stripDelimiter this flag is ignored
237         * @deprecated as of Spring 5.0.4, in favor of {@link #allMimeTypes()} or
238         * {@link #allMimeTypes(List, boolean)}
239         */
240        @Deprecated
241        public static StringDecoder allMimeTypes(boolean stripDelimiter) {
242                return allMimeTypes();
243        }
244
245        /**
246         * Create a {@code StringDecoder} that supports all MIME types.
247         */
248        public static StringDecoder allMimeTypes() {
249                return allMimeTypes(DEFAULT_DELIMITERS, true);
250        }
251
252        /**
253         * Create a {@code StringDecoder} that supports all MIME types.
254         * @param delimiters delimiter strings to use to split the input stream
255         * @param stripDelimiter whether to remove delimiters from the resulting
256         * input strings
257         */
258        public static StringDecoder allMimeTypes(List<String> delimiters, boolean stripDelimiter) {
259                return new StringDecoder(delimiters, stripDelimiter,
260                                new MimeType("text", "plain", DEFAULT_CHARSET), MimeTypeUtils.ALL);
261        }
262
263}