@ -21,21 +21,20 @@ import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets ;
import java.nio.charset.StandardCharsets ;
import java.util.ArrayList ;
import java.util.ArrayList ;
import java.util.Arrays ;
import java.util.Arrays ;
import java.util.Collection ;
import java.util.Collections ;
import java.util.List ;
import java.util.List ;
import java.util.Map ;
import java.util.Map ;
import java.util.concurrent.ConcurrentHashMap ;
import java.util.concurrent.ConcurrentHashMap ;
import java.util.concurrent.ConcurrentMap ;
import java.util.concurrent.ConcurrentMap ;
import java.util.function.Consumer ;
import org.reactivestreams.Publisher ;
import org.reactivestreams.Publisher ;
import reactor.core.publisher.Flux ;
import reactor.core.publisher.Flux ;
import reactor.core.publisher.Mono ;
import org.springframework.core.ResolvableType ;
import org.springframework.core.ResolvableType ;
import org.springframework.core.io.buffer.DataBuffer ;
import org.springframework.core.io.buffer.DataBuffer ;
import org.springframework.core.io.buffer.DataBufferLimitException ;
import org.springframework.core.io.buffer.DataBufferUtils ;
import org.springframework.core.io.buffer.DataBufferUtils ;
import org.springframework.core.io.buffer.DataBufferWrapper ;
import org.springframework.core.io.buffer.DefaultDataBufferFactory ;
import org.springframework.core.io.buffer.LimitedDataBufferList ;
import org.springframework.core.io.buffer.LimitedDataBufferList ;
import org.springframework.core.io.buffer.PooledDataBuffer ;
import org.springframework.core.io.buffer.PooledDataBuffer ;
import org.springframework.core.log.LogFormatUtils ;
import org.springframework.core.log.LogFormatUtils ;
@ -45,12 +44,12 @@ import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils ;
import org.springframework.util.MimeTypeUtils ;
/ * *
/ * *
* Decode from a data buffer stream to a { @code String } stream . Before decoding , this decoder
* Decode from a data buffer stream to a { @code String } stream , either splitting
* realigns the incoming data buffers so that each buffer ends with a newline .
* or aggregating incoming data chunks to realign along newlines delimiters
* This is to make sure that multibyte characters are decoded properly , and do not cross buffer
* and produce a stream of strings . This is useful for streaming but is also
* boundaries . The default delimiters ( { @code \ n } , { @code \ r \ n } ) can be customized .
* necessary to ensure that that multibyte characters can be decoded correctly ,
*
* avoiding split - character issues . The default delimiters used by default are
* < p > Partially inspired by Netty ' s { @code DelimiterBasedFrameDecoder } .
* { @code \ n } and { @code \ r \ n } but that can be customized .
*
*
* @author Sebastien Deleuze
* @author Sebastien Deleuze
* @author Brian Clozel
* @author Brian Clozel
@ -115,21 +114,22 @@ public final class StringDecoder extends AbstractDataBufferDecoder<String> {
byte [ ] [ ] delimiterBytes = getDelimiterBytes ( mimeType ) ;
byte [ ] [ ] delimiterBytes = getDelimiterBytes ( mimeType ) ;
Flux < DataBuffer > inputFlux = Flux . defer ( ( ) - > {
LimitedDataBufferList chunks = new LimitedDataBufferList ( getMaxInMemorySize ( ) ) ;
DataBufferUtils . Matcher matcher = DataBufferUtils . matcher ( delimiterBytes ) ;
DataBufferUtils . Matcher matcher = DataBufferUtils . matcher ( delimiterBytes ) ;
@SuppressWarnings ( "MismatchedQueryAndUpdateOfCollection" )
return Flux . from ( input )
LimitChecker limiter = new LimitChecker ( getMaxInMemorySize ( ) ) ;
. concatMapIterable ( buffer - > processDataBuffer ( buffer , matcher , chunks ) )
. concatWith ( Mono . defer ( ( ) - > {
return Flux . from ( input )
if ( chunks . isEmpty ( ) ) {
. concatMapIterable ( buffer - > endFrameAfterDelimiter ( buffer , matcher ) )
return Mono . empty ( ) ;
. doOnNext ( limiter )
}
. bufferUntil ( buffer - > buffer instanceof EndFrameBuffer )
DataBuffer lastBuffer = chunks . get ( 0 ) . factory ( ) . join ( chunks ) ;
. map ( list - > joinAndStrip ( list , this . stripDelimiter ) )
chunks . clear ( ) ;
. doOnDiscard ( PooledDataBuffer . class , DataBufferUtils : : release ) ;
return Mono . just ( lastBuffer ) ;
} ) ;
} ) )
. doOnTerminate ( chunks : : releaseAndClear )
return super . decode ( inputFlux , elementType , mimeType , hints ) ;
. doOnDiscard ( PooledDataBuffer . class , PooledDataBuffer : : release )
. map ( buffer - > decode ( buffer , elementType , mimeType , hints ) ) ;
}
}
private byte [ ] [ ] getDelimiterBytes ( @Nullable MimeType mimeType ) {
private byte [ ] [ ] getDelimiterBytes ( @Nullable MimeType mimeType ) {
@ -142,6 +142,43 @@ public final class StringDecoder extends AbstractDataBufferDecoder<String> {
} ) ;
} ) ;
}
}
private Collection < DataBuffer > processDataBuffer (
DataBuffer buffer , DataBufferUtils . Matcher matcher , LimitedDataBufferList chunks ) {
try {
List < DataBuffer > result = null ;
do {
int endIndex = matcher . match ( buffer ) ;
if ( endIndex = = - 1 ) {
chunks . add ( buffer ) ;
DataBufferUtils . retain ( buffer ) ; // retain after add (may raise DataBufferLimitException)
break ;
}
int startIndex = buffer . readPosition ( ) ;
int length = ( endIndex - startIndex + 1 ) ;
DataBuffer slice = buffer . retainedSlice ( startIndex , length ) ;
if ( this . stripDelimiter ) {
slice . writePosition ( slice . writePosition ( ) - matcher . delimiter ( ) . length ) ;
}
result = ( result ! = null ? result : new ArrayList < > ( ) ) ;
if ( chunks . isEmpty ( ) ) {
result . add ( slice ) ;
}
else {
chunks . add ( slice ) ;
result . add ( buffer . factory ( ) . join ( chunks ) ) ;
chunks . clear ( ) ;
}
buffer . readPosition ( endIndex + 1 ) ;
}
while ( buffer . readableByteCount ( ) > 0 ) ;
return ( result ! = null ? result : Collections . emptyList ( ) ) ;
}
finally {
DataBufferUtils . release ( buffer ) ;
}
}
@Override
@Override
public String decode ( DataBuffer dataBuffer , ResolvableType elementType ,
public String decode ( DataBuffer dataBuffer , ResolvableType elementType ,
@Nullable MimeType mimeType , @Nullable Map < String , Object > hints ) {
@Nullable MimeType mimeType , @Nullable Map < String , Object > hints ) {
@ -166,68 +203,6 @@ public final class StringDecoder extends AbstractDataBufferDecoder<String> {
}
}
}
}
/ * *
* Finds the first match and longest delimiter , { @link EndFrameBuffer } just after it .
* @param dataBuffer the buffer to find delimiters in
* @param matcher used to find the first delimiters
* @return a flux of buffers , containing { @link EndFrameBuffer } after each delimiter that was
* found in { @code dataBuffer } . Returns Flux , because returning List ( w / flatMapIterable )
* results in memory leaks due to pre - fetching .
* /
private static List < DataBuffer > endFrameAfterDelimiter ( DataBuffer dataBuffer , DataBufferUtils . Matcher matcher ) {
List < DataBuffer > result = new ArrayList < > ( ) ;
try {
do {
int endIdx = matcher . match ( dataBuffer ) ;
if ( endIdx ! = - 1 ) {
int readPosition = dataBuffer . readPosition ( ) ;
int length = ( endIdx - readPosition + 1 ) ;
DataBuffer slice = dataBuffer . retainedSlice ( readPosition , length ) ;
result . add ( slice ) ;
result . add ( new EndFrameBuffer ( matcher . delimiter ( ) ) ) ;
dataBuffer . readPosition ( endIdx + 1 ) ;
}
else {
result . add ( DataBufferUtils . retain ( dataBuffer ) ) ;
break ;
}
}
while ( dataBuffer . readableByteCount ( ) > 0 ) ;
}
finally {
DataBufferUtils . release ( dataBuffer ) ;
}
return result ;
}
/ * *
* Joins the given list of buffers . If the list ends with a { @link EndFrameBuffer } , it is
* removed . If { @code stripDelimiter } is { @code true } and the resulting buffer ends with
* a delimiter , it is removed .
* @param dataBuffers the data buffers to join
* @param stripDelimiter whether to strip the delimiter
* @return the joined buffer
* /
private static DataBuffer joinAndStrip ( List < DataBuffer > dataBuffers , boolean stripDelimiter ) {
Assert . state ( ! dataBuffers . isEmpty ( ) , "DataBuffers should not be empty" ) ;
byte [ ] matchingDelimiter = null ;
int lastIdx = dataBuffers . size ( ) - 1 ;
DataBuffer lastBuffer = dataBuffers . get ( lastIdx ) ;
if ( lastBuffer instanceof EndFrameBuffer ) {
matchingDelimiter = ( ( EndFrameBuffer ) lastBuffer ) . delimiter ( ) ;
dataBuffers . remove ( lastIdx ) ;
}
DataBuffer result = dataBuffers . get ( 0 ) . factory ( ) . join ( dataBuffers ) ;
if ( stripDelimiter & & matchingDelimiter ! = null ) {
result . writePosition ( result . writePosition ( ) - matchingDelimiter . length ) ;
}
return result ;
}
/ * *
/ * *
* Create a { @code StringDecoder } for { @code "text/plain" } .
* Create a { @code StringDecoder } for { @code "text/plain" } .
* @param stripDelimiter this flag is ignored
* @param stripDelimiter this flag is ignored
@ -285,46 +260,4 @@ public final class StringDecoder extends AbstractDataBufferDecoder<String> {
new MimeType ( "text" , "plain" , DEFAULT_CHARSET ) , MimeTypeUtils . ALL ) ;
new MimeType ( "text" , "plain" , DEFAULT_CHARSET ) , MimeTypeUtils . ALL ) ;
}
}
private static class EndFrameBuffer extends DataBufferWrapper {
private static final DataBuffer BUFFER = new DefaultDataBufferFactory ( ) . wrap ( new byte [ 0 ] ) ;
private final byte [ ] delimiter ;
public EndFrameBuffer ( byte [ ] delimiter ) {
super ( BUFFER ) ;
this . delimiter = delimiter ;
}
public byte [ ] delimiter ( ) {
return this . delimiter ;
}
}
private static class LimitChecker implements Consumer < DataBuffer > {
@SuppressWarnings ( "MismatchedQueryAndUpdateOfCollection" )
private final LimitedDataBufferList list ;
LimitChecker ( int maxInMemorySize ) {
this . list = new LimitedDataBufferList ( maxInMemorySize ) ;
}
@Override
public void accept ( DataBuffer buffer ) {
if ( buffer instanceof EndFrameBuffer ) {
this . list . clear ( ) ;
}
try {
this . list . add ( buffer ) ;
}
catch ( DataBufferLimitException ex ) {
DataBufferUtils . release ( buffer ) ;
throw ex ;
}
}
}
}
}