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}