@ -17,19 +17,16 @@
package org.springframework.http.codec ;
package org.springframework.http.codec ;
import java.nio.charset.StandardCharsets ;
import java.nio.charset.StandardCharsets ;
import java.util.ArrayList ;
import java.util.Collections ;
import java.util.Collections ;
import java.util.HashMap ;
import java.util.HashMap ;
import java.util.List ;
import java.util.List ;
import java.util.Map ;
import java.util.Map ;
import java.util.Optional ;
import org.reactivestreams.Publisher ;
import org.reactivestreams.Publisher ;
import reactor.core.publisher.Flux ;
import reactor.core.publisher.Flux ;
import reactor.core.publisher.Mono ;
import reactor.core.publisher.Mono ;
import org.springframework.core.ResolvableType ;
import org.springframework.core.ResolvableType ;
import org.springframework.core.codec.CodecException ;
import org.springframework.core.codec.Encoder ;
import org.springframework.core.codec.Encoder ;
import org.springframework.core.io.buffer.DataBuffer ;
import org.springframework.core.io.buffer.DataBuffer ;
import org.springframework.core.io.buffer.DataBufferFactory ;
import org.springframework.core.io.buffer.DataBufferFactory ;
@ -38,39 +35,37 @@ import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.server.reactive.ServerHttpRequest ;
import org.springframework.http.server.reactive.ServerHttpRequest ;
import org.springframework.http.server.reactive.ServerHttpResponse ;
import org.springframework.http.server.reactive.ServerHttpResponse ;
import org.springframework.util.Assert ;
import org.springframework.util.Assert ;
import org.springframework.util.MimeTypeUtils ;
/ * *
/ * *
* Writer that supports a stream of { @link ServerSentEvent } s and also plain
* { @code ServerHttpMessageWriter } for { @code "text/event-stream" } responses .
* { @link Object } s which is the same as an { @link ServerSentEvent } with data
* only .
*
*
* @author Sebastien Deleuze
* @author Sebastien Deleuze
* @author Arjen Poutsma
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @since 5 . 0
* @since 5 . 0
* /
* /
public class ServerSentEventHttpMessageWriter implements ServerHttpMessageWriter < Object > {
public class ServerSentEventHttpMessageWriter implements ServerHttpMessageWriter < Object > {
/ * *
private static final List < MediaType > WRITABLE_MEDIA_TYPES =
* Server - Sent Events hint key expecting a { @link Boolean } value which when set to true
Collections . singletonList ( MediaType . TEXT_EVENT_STREAM ) ;
* will adapt the content in order to comply with Server - Sent Events recommendation .
* For example , it will append "data:" after each line break with data encoders
* supporting it .
* @see < a href = "https://www.w3.org/TR/eventsource/" > Server - Sent Events W3C recommendation < / a >
* /
public static final String SSE_CONTENT_HINT = ServerSentEventHttpMessageWriter . class . getName ( ) + ".sseContent" ;
private final List < Encoder < ? > > dataEncoders ;
private final Encoder < ? > encoder ;
public ServerSentEventHttpMessageWriter ( ) {
/ * *
this . dataEncoders = Collections . emptyList ( ) ;
* Constructor with JSON { @code Encoder } for encoding objects . Support for
* { @code String } event data is built - in .
* /
public ServerSentEventHttpMessageWriter ( Encoder < ? > encoder ) {
Assert . notNull ( encoder , "'encoder' must not be null" ) ;
this . encoder = encoder ;
}
}
public ServerSentEventHttpMessageWriter ( List < Encoder < ? > > dataEncoders ) {
Assert . notNull ( dataEncoders , "'dataEncoders' must not be null" ) ;
@Override
this . dataEncoders = new ArrayList < > ( dataEncoders ) ;
public List < MediaType > getWritableMediaTypes ( ) {
return WRITABLE_MEDIA_TYPES ;
}
}
@ -81,61 +76,35 @@ public class ServerSentEventHttpMessageWriter implements ServerHttpMessageWriter
}
}
@Override
@Override
public List < MediaType > getWritableMediaTypes ( ) {
public Mono < Void > write ( Publisher < ? > input , ResolvableType elementType , MediaType mediaType ,
return Collections . singletonList ( MediaType . TEXT_EVENT_STREAM ) ;
}
@Override
public Mono < Void > write ( Publisher < ? > inputStream , ResolvableType elementType , MediaType mediaType ,
ReactiveHttpOutputMessage message , Map < String , Object > hints ) {
ReactiveHttpOutputMessage message , Map < String , Object > hints ) {
message . getHeaders ( ) . setContentType ( MediaType . TEXT_EVENT_STREAM ) ;
message . getHeaders ( ) . setContentType ( MediaType . TEXT_EVENT_STREAM ) ;
return message . writeAndFlushWith ( encode ( input , message . bufferFactory ( ) , elementType , hints ) ) ;
}
DataBufferFactory bufferFactory = message . bufferFactory ( ) ;
private Flux < Publisher < DataBuffer > > encode ( Publisher < ? > input , DataBufferFactory factory ,
Flux < Publisher < DataBuffer > > body = encode ( inputStream , bufferFactory , elementType , hints ) ;
ResolvableType elementType , Map < String , Object > hints ) {
return message . writeAndFlushWith ( body ) ;
ResolvableType valueType = ServerSentEvent . class . isAssignableFrom ( elementType . getRawClass ( ) ) ?
}
elementType . getGeneric ( 0 ) : elementType ;
private Flux < Publisher < DataBuffer > > encode ( Publisher < ? > inputStream , DataBufferFactory bufferFactory ,
return Flux . from ( input ) . map ( element - > {
ResolvableType type , Map < String , Object > hints ) {
Map < String , Object > hintsWithSse = new HashMap < > ( hints ) ;
hintsWithSse . put ( SSE_CONTENT_HINT , true ) ;
return Flux . from ( inputStream )
. map ( o - > toSseEvent ( o , type ) )
. map ( sse - > {
StringBuilder sb = new StringBuilder ( ) ;
sse . id ( ) . ifPresent ( id - > writeField ( "id" , id , sb ) ) ;
sse . event ( ) . ifPresent ( event - > writeField ( "event" , event , sb ) ) ;
sse . retry ( ) . ifPresent ( retry - > writeField ( "retry" , retry . toMillis ( ) , sb ) ) ;
sse . comment ( ) . ifPresent ( comment - > {
comment = comment . replaceAll ( "\\n" , "\n:" ) ;
sb . append ( ':' ) . append ( comment ) . append ( "\n" ) ;
} ) ;
Flux < DataBuffer > dataBuffer = sse . data ( )
. < Flux < DataBuffer > > map ( data - > {
sb . append ( "data:" ) ;
if ( data instanceof String ) {
String stringData = ( ( String ) data ) . replaceAll ( "\\n" , "\ndata:" ) ;
sb . append ( stringData ) . append ( '\n' ) ;
return Flux . empty ( ) ;
}
else {
return applyEncoder ( data , bufferFactory , hintsWithSse ) ;
}
} ) . orElse ( Flux . empty ( ) ) ;
return Flux . concat ( encodeString ( sb . toString ( ) , bufferFactory ) , dataBuffer ,
encodeString ( "\n" , bufferFactory ) ) ;
} ) ;
}
ServerSentEvent < ? > sse = element instanceof ServerSentEvent ?
( ServerSentEvent < ? > ) element : ServerSentEvent . builder ( ) . data ( element ) . build ( ) ;
StringBuilder sb = new StringBuilder ( ) ;
sse . id ( ) . ifPresent ( v - > writeField ( "id" , v , sb ) ) ;
sse . event ( ) . ifPresent ( v - > writeField ( "event" , v , sb ) ) ;
sse . retry ( ) . ifPresent ( v - > writeField ( "retry" , v . toMillis ( ) , sb ) ) ;
sse . comment ( ) . ifPresent ( v - > sb . append ( ':' ) . append ( v . replaceAll ( "\\n" , "\n:" ) ) . append ( "\n" ) ) ;
sse . data ( ) . ifPresent ( v - > sb . append ( "data:" ) ) ;
private ServerSentEvent < ? > toSseEvent ( Object data , ResolvableType type ) {
return Flux . concat ( encodeText ( sb , factory ) ,
return ServerSentEvent . class . isAssignableFrom ( type . getRawClass ( ) )
encodeData ( sse , valueType , factory , hints ) ,
? ( ServerSentEvent < ? > ) data
encodeText ( "\n" , factory ) ) ;
: ServerSentEvent . builder ( ) . data ( data ) . build ( ) ;
} ) ;
}
}
private void writeField ( String fieldName , Object fieldValue , StringBuilder stringBuilder ) {
private void writeField ( String fieldName , Object fieldValue , StringBuilder stringBuilder ) {
@ -146,40 +115,50 @@ public class ServerSentEventHttpMessageWriter implements ServerHttpMessageWriter
}
}
@SuppressWarnings ( "unchecked" )
@SuppressWarnings ( "unchecked" )
private < T > Flux < DataBuffer > applyEncoder ( Object data , DataBufferFactory bufferFactory , Map < String , Object > hints ) {
private < T > Flux < DataBuffer > encodeData ( ServerSentEvent < ? > event , ResolvableType valueType ,
ResolvableType elementType = ResolvableType . forClass ( data . getClass ( ) ) ;
DataBufferFactory factory , Map < String , Object > hints ) {
Optional < Encoder < ? > > encoder = dataEncoders
. stream ( )
Object data = event . data ( ) . orElse ( null ) ;
. filter ( e - > e . canEncode ( elementType , MimeTypeUtils . APPLICATION_JSON ) )
if ( data = = null ) {
. findFirst ( ) ;
return Flux . empty ( ) ;
return ( ( Encoder < T > ) encoder . orElseThrow ( ( ) - > new CodecException ( "No suitable encoder found!" ) ) )
}
. encode ( Mono . just ( ( T ) data ) , bufferFactory , elementType , MimeTypeUtils . APPLICATION_JSON , hints )
. concatWith ( encodeString ( "\n" , bufferFactory ) ) ;
if ( data instanceof String ) {
String text = ( String ) data ;
return Flux . from ( encodeText ( text . replaceAll ( "\\n" , "\ndata:" ) + "\n" , factory ) ) ;
}
return ( ( Encoder < T > ) this . encoder )
. encode ( Mono . just ( ( T ) data ) , factory , valueType , MediaType . TEXT_EVENT_STREAM , hints )
. concatWith ( encodeText ( "\n" , factory ) ) ;
}
}
private Mono < DataBuffer > encodeString ( String str , DataBufferFactory bufferFactory ) {
private Mono < DataBuffer > encodeText ( CharSequence text , DataBufferFactory bufferFactory ) {
byte [ ] bytes = str . getBytes ( StandardCharsets . UTF_8 ) ;
byte [ ] bytes = text . toS tring ( ) . getBytes ( StandardCharsets . UTF_8 ) ;
DataBuffer buffer = bufferFactory . allocateBuffer ( bytes . length ) . write ( bytes ) ;
DataBuffer buffer = bufferFactory . allocateBuffer ( bytes . length ) . write ( bytes ) ;
return Mono . just ( buffer ) ;
return Mono . just ( buffer ) ;
}
}
@Override
@Override
public Mono < Void > write ( Publisher < ? > inputStream , ResolvableType actualType , ResolvableType elementType ,
public Mono < Void > write ( Publisher < ? > input , ResolvableType actualType , ResolvableType elementType ,
MediaType mediaType , ServerHttpRequest request , ServerHttpResponse response ,
MediaType mediaType , ServerHttpRequest request , ServerHttpResponse response ,
Map < String , Object > hints ) {
Map < String , Object > hints ) {
Map < String , Object > allHints = this . dataEncoders . stream ( )
Map < String , Object > allHints = new HashMap < > ( ) ;
. filter ( encoder - > encoder instanceof ServerHttpEncoder )
allHints . putAll ( getEncodeHints ( actualType , elementType , mediaType , request , response ) ) ;
. map ( encoder - > ( ServerHttpEncoder < ? > ) encoder )
. map ( encoder - > encoder . getEncodeHints ( actualType , elementType , mediaType , request , response ) )
. reduce ( new HashMap < > ( ) , ( t , u ) - > {
t . putAll ( u ) ;
return t ;
} ) ;
allHints . putAll ( hints ) ;
allHints . putAll ( hints ) ;
return write ( inputStream , elementType , mediaType , response , allHints ) ;
return write ( input , elementType , mediaType , response , allHints ) ;
}
private Map < String , Object > getEncodeHints ( ResolvableType actualType , ResolvableType elementType ,
MediaType mediaType , ServerHttpRequest request , ServerHttpResponse response ) {
if ( this . encoder instanceof ServerHttpEncoder ) {
ServerHttpEncoder < ? > httpEncoder = ( ServerHttpEncoder < ? > ) this . encoder ;
return httpEncoder . getEncodeHints ( actualType , elementType , mediaType , request , response ) ;
}
return Collections . emptyMap ( ) ;
}
}
}
}