@ -15,26 +15,53 @@
@@ -15,26 +15,53 @@
* /
package org.springframework.web.reactive.result.method.annotation ;
import java.net.URI ;
import java.nio.ByteBuffer ;
import java.nio.charset.Charset ;
import java.time.Duration ;
import java.util.ArrayList ;
import java.util.List ;
import java.util.concurrent.CompletableFuture ;
import java.util.function.Predicate ;
import org.junit.Before ;
import org.junit.Test ;
import reactor.core.converter.RxJava1ObservableConverter ;
import reactor.core.converter.RxJava1SingleConverter ;
import reactor.core.publisher.Flux ;
import reactor.core.publisher.Mono ;
import reactor.core.test.TestSubscriber ;
import rx.Observable ;
import rx.Single ;
import org.springframework.core.MethodParameter ;
import org.springframework.core.ResolvableType ;
import org.springframework.core.codec.StringDecoder ;
import org.springframework.core.convert.support.MonoToCompletableFutureConverter ;
import org.springframework.core.convert.support.ReactorToRxJava1Converter ;
import org.springframework.core.io.buffer.DataBuffer ;
import org.springframework.core.io.buffer.DefaultDataBufferFactory ;
import org.springframework.format.support.DefaultFormattingConversionService ;
import org.springframework.format.support.FormattingConversionService ;
import org.springframework.http.HttpMethod ;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter ;
import org.springframework.http.converter.reactive.HttpMessageConverter ;
import org.springframework.http.server.reactive.MockServerHttpRequest ;
import org.springframework.http.server.reactive.MockServerHttpResponse ;
import org.springframework.ui.ExtendedModelMap ;
import org.springframework.web.bind.annotation.RequestBody ;
import org.springframework.web.reactive.result.ResolvableMethod ;
import org.springframework.web.server.ServerWebExchange ;
import org.springframework.web.server.ServerWebInputException ;
import org.springframework.web.server.adapter.DefaultServerWebExchange ;
import org.springframework.web.server.session.MockWebSessionManager ;
import static org.junit.Assert.assertEquals ;
import static org.junit.Assert.assertFalse ;
import static org.junit.Assert.assertNotNull ;
import static org.junit.Assert.assertNull ;
import static org.junit.Assert.assertTrue ;
import static org.springframework.core.ResolvableType.forClass ;
import static org.springframework.core.ResolvableType.forClassWithGenerics ;
/ * *
@ -46,21 +73,130 @@ import static org.springframework.core.ResolvableType.forClassWithGenerics;
@@ -46,21 +73,130 @@ import static org.springframework.core.ResolvableType.forClassWithGenerics;
* /
public class RequestBodyArgumentResolverTests {
private RequestBodyArgumentResolver resolver = resolver ( ) ;
private ServerWebExchange exchange ;
private MockServerHttpRequest request ;
private ResolvableMethod testMethod = ResolvableMethod . on ( this . getClass ( ) ) . name ( "handle" ) ;
@Before
public void setUp ( ) throws Exception {
this . request = new MockServerHttpRequest ( HttpMethod . POST , new URI ( "/path" ) ) ;
MockServerHttpResponse response = new MockServerHttpResponse ( ) ;
this . exchange = new DefaultServerWebExchange ( this . request , response , new MockWebSessionManager ( ) ) ;
}
@Test
public void supports ( ) throws Exception {
ResolvableType type = forClassWithGenerics ( Mono . class , String . class ) ;
MethodParameter param = this . testMethod . resolveParam ( type , requestBody ( true ) ) ;
assertTrue ( this . resolver . supportsParameter ( param ) ) ;
MethodParameter parameter = this . testMethod . resolveParam ( p - > ! p . hasParameterAnnotations ( ) ) ;
assertFalse ( this . resolver . supportsParameter ( parameter ) ) ;
}
ResolvableMethod testMethod = ResolvableMethod . on ( getClass ( ) ) . name ( "handle" ) ;
RequestBodyArgumentResolver resolver = resolver ( ) ;
@Test
public void stringBody ( ) throws Exception {
String body = "line1" ;
ResolvableType type = forClass ( String . class ) ;
MethodParameter param = this . testMethod . resolveParam ( type , requestBody ( true ) ) ;
String value = resolveValue ( param , body ) ;
assertEquals ( body , value ) ;
}
@Test ( expected = ServerWebInputException . class )
public void emptyBodyWithString ( ) throws Exception {
resolveValueWithEmptyBody ( forClass ( String . class ) , true ) ;
}
@Test
public void emptyBodyWithStringNotRequired ( ) throws Exception {
ResolvableType type = forClass ( String . class ) ;
String body = resolveValueWithEmptyBody ( type , false ) ;
assertNull ( body ) ;
}
@Test
public void emptyBodyWithMono ( ) throws Exception {
ResolvableType type = forClassWithGenerics ( Mono . class , String . class ) ;
MethodParameter param = testMethod . resolveParam ( type ) ;
assertTrue ( resolver . supportsParameter ( param ) ) ;
MethodParameter parameter = testMethod . resolveParam ( p - > ! p . hasParameterAnnotations ( ) ) ;
assertFalse ( resolver . supportsParameter ( parameter ) ) ;
TestSubscriber . subscribe ( resolveValueWithEmptyBody ( type , true ) )
. assertNoValues ( )
. assertError ( ServerWebInputException . class ) ;
TestSubscriber . subscribe ( resolveValueWithEmptyBody ( type , false ) )
. assertNoValues ( )
. assertComplete ( ) ;
}
@Test
public void emptyBodyWithFlux ( ) throws Exception {
ResolvableType type = forClassWithGenerics ( Flux . class , String . class ) ;
TestSubscriber . subscribe ( resolveValueWithEmptyBody ( type , true ) )
. assertNoValues ( )
. assertError ( ServerWebInputException . class ) ;
TestSubscriber . subscribe ( resolveValueWithEmptyBody ( type , false ) )
. assertNoValues ( )
. assertComplete ( ) ;
}
@Test
public void emptyBodyWithSingle ( ) throws Exception {
ResolvableType type = forClassWithGenerics ( Single . class , String . class ) ;
Single < String > single = resolveValueWithEmptyBody ( type , true ) ;
TestSubscriber . subscribe ( RxJava1SingleConverter . from ( single ) )
. assertNoValues ( )
. assertError ( ServerWebInputException . class ) ;
single = resolveValueWithEmptyBody ( type , false ) ;
TestSubscriber . subscribe ( RxJava1SingleConverter . from ( single ) )
. assertNoValues ( )
. assertError ( ServerWebInputException . class ) ;
}
@Test
public void emptyBodyWithObservable ( ) throws Exception {
ResolvableType type = forClassWithGenerics ( Observable . class , String . class ) ;
Observable < String > observable = resolveValueWithEmptyBody ( type , true ) ;
TestSubscriber . subscribe ( RxJava1ObservableConverter . from ( observable ) )
. assertNoValues ( )
. assertError ( ServerWebInputException . class ) ;
observable = resolveValueWithEmptyBody ( type , false ) ;
TestSubscriber . subscribe ( RxJava1ObservableConverter . from ( observable ) )
. assertNoValues ( )
. assertComplete ( ) ;
}
@Test
public void emptyBodyWithCompletableFuture ( ) throws Exception {
ResolvableType type = forClassWithGenerics ( CompletableFuture . class , String . class ) ;
CompletableFuture < String > future = resolveValueWithEmptyBody ( type , true ) ;
future . whenComplete ( ( text , ex ) - > {
assertNull ( text ) ;
assertNotNull ( ex ) ;
} ) ;
future = resolveValueWithEmptyBody ( type , false ) ;
future . whenComplete ( ( text , ex ) - > {
assertNotNull ( text ) ;
assertNull ( ex ) ;
} ) ;
}
private RequestBodyArgumentResolver resolver ( ) {
List < HttpMessageConverter < ? > > converters = new ArrayList < > ( ) ;
converters . add ( new CodecHttpMessageConverter < > ( new StringDecoder ( ) ) ) ;
@ -72,8 +208,62 @@ public class RequestBodyArgumentResolverTests {
@@ -72,8 +208,62 @@ public class RequestBodyArgumentResolverTests {
return new RequestBodyArgumentResolver ( converters , service ) ;
}
private < T > T resolveValue ( MethodParameter param , String body ) {
this . request . writeWith ( Flux . just ( dataBuffer ( body ) ) ) ;
Mono < Object > result = this . resolver . readBody ( param , true , this . exchange ) ;
Object value = result . block ( Duration . ofSeconds ( 5 ) ) ;
assertNotNull ( value ) ;
assertTrue ( "Unexpected return value type: " + value ,
param . getParameterType ( ) . isAssignableFrom ( value . getClass ( ) ) ) ;
//noinspection unchecked
return ( T ) value ;
}
private < T > T resolveValueWithEmptyBody ( ResolvableType type , boolean required ) {
this . request . writeWith ( Flux . empty ( ) ) ;
MethodParameter param = this . testMethod . resolveParam ( type , requestBody ( required ) ) ;
Mono < Object > result = this . resolver . resolveArgument ( param , new ExtendedModelMap ( ) , this . exchange ) ;
Object value = result . block ( Duration . ofSeconds ( 5 ) ) ;
if ( value ! = null ) {
assertTrue ( "Unexpected return value type: " + value ,
param . getParameterType ( ) . isAssignableFrom ( value . getClass ( ) ) ) ;
}
//noinspection unchecked
return ( T ) value ;
}
private Predicate < MethodParameter > requestBody ( boolean required ) {
return p - > {
RequestBody annotation = p . getParameterAnnotation ( RequestBody . class ) ;
return annotation ! = null & & annotation . required ( ) = = required ;
} ;
}
private DataBuffer dataBuffer ( String body ) {
byte [ ] bytes = body . getBytes ( Charset . forName ( "UTF-8" ) ) ;
ByteBuffer byteBuffer = ByteBuffer . wrap ( bytes ) ;
return new DefaultDataBufferFactory ( ) . wrap ( byteBuffer ) ;
}
@SuppressWarnings ( "unused" )
void handle ( @RequestBody Mono < String > monoString , String paramWithoutAnnotation ) { }
void handle (
@RequestBody String string ,
@RequestBody Mono < String > mono ,
@RequestBody Flux < String > flux ,
@RequestBody Single < String > single ,
@RequestBody Observable < String > obs ,
@RequestBody CompletableFuture < String > future ,
@RequestBody ( required = false ) String stringNotRequired ,
@RequestBody ( required = false ) Mono < String > monoNotRequired ,
@RequestBody ( required = false ) Flux < String > fluxNotRequired ,
@RequestBody ( required = false ) Single < String > singleNotRequired ,
@RequestBody ( required = false ) Observable < String > obsNotRequired ,
@RequestBody ( required = false ) CompletableFuture < String > futureNotRequired ,
String notAnnotated ) { }
}