From 6a28f06b2e2f9add0358daaf73552bfdcd8a9de5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 2 Jan 2026 16:44:02 +0100 Subject: [PATCH] Extract some WebFlux functional snippets See gh-36089 --- .../ROOT/pages/web/webflux-functional.adoc | 375 +----------------- .../web/webfluxfnhandlerclasses/Person.java | 19 + .../PersonHandler.java | 67 ++++ .../PersonRepository.java | 29 ++ .../RouterConfiguration.java | 55 +++ .../SecurityManager.java | 22 + .../PersonHandler.java | 58 +++ .../PersonValidator.java | 34 ++ .../RouterConfiguration.java | 36 ++ .../webfluxfnrequest/PartEventHandler.java | 55 +++ .../web/webfluxfnrequest/RequestHandler.java | 33 ++ .../webfluxfnresponse/ResponseHandler.java | 37 ++ .../webfluxfnroutes/RouterConfiguration.java | 78 ++++ .../web/webfluxfnhandlerclasses/Person.kt | 19 + .../webfluxfnhandlerclasses/PersonHandler.kt | 62 +++ .../PersonRepository.kt | 28 ++ .../RouterConfiguration.kt | 58 +++ .../SecurityManager.kt | 22 + .../PersonHandler.kt | 53 +++ .../PersonValidator.kt | 32 ++ .../RouterConfiguration.kt | 37 ++ .../web/webfluxfnrequest/PartEventHandler.kt | 55 +++ .../web/webfluxfnrequest/RequestHandler.kt | 31 ++ .../web/webfluxfnresponse/ResponseHandler.kt | 35 ++ .../webfluxfnroutes/RouterConfiguration.kt | 65 +++ 25 files changed, 1032 insertions(+), 363 deletions(-) create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerclasses/Person.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerclasses/PersonHandler.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerclasses/PersonRepository.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerfilterfunction/RouterConfiguration.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerfilterfunction/SecurityManager.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlervalidation/PersonHandler.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlervalidation/PersonValidator.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webfluxfnpredicates/RouterConfiguration.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webfluxfnrequest/PartEventHandler.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webfluxfnrequest/RequestHandler.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webfluxfnresponse/ResponseHandler.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webfluxfnroutes/RouterConfiguration.java create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerclasses/Person.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerclasses/PersonHandler.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerclasses/PersonRepository.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerfilterfunction/RouterConfiguration.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerfilterfunction/SecurityManager.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlervalidation/PersonHandler.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlervalidation/PersonValidator.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnpredicates/RouterConfiguration.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnrequest/PartEventHandler.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnrequest/RequestHandler.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnresponse/ResponseHandler.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnroutes/RouterConfiguration.kt diff --git a/framework-docs/modules/ROOT/pages/web/webflux-functional.adoc b/framework-docs/modules/ROOT/pages/web/webflux-functional.adoc index 6b2247f4b29..ef9d0b03f5c 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-functional.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-functional.adoc @@ -235,87 +235,14 @@ val map = request.awaitMultipartData() The following example shows how to access multipart data, one at a time, in streaming fashion: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes"] ----- -Flux allPartEvents = request.bodyToFlux(PartEvent.class); -allPartEvents.windowUntil(PartEvent::isLast) - .concatMap(p -> p.switchOnFirst((signal, partEvents) -> { - if (signal.hasValue()) { - PartEvent event = signal.get(); - if (event instanceof FormPartEvent formEvent) { - String value = formEvent.value(); - // handle form field - } - else if (event instanceof FilePartEvent fileEvent) { - String filename = fileEvent.filename(); - Flux contents = partEvents.map(PartEvent::content); - // handle file upload - } - else { - return Mono.error(new RuntimeException("Unexpected event: " + event)); - } - } - else { - return partEvents; // either complete or error signal - } - })); ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes"] ----- -val allPartEvents = request.bodyToFlux() -allPartEvents.windowUntil(PartEvent::isLast) - .concatMap { - it.switchOnFirst { signal, partEvents -> - if (signal.hasValue()) { - val event = signal.get() - if (event is FormPartEvent) { - val value: String = event.value() - // handle form field - } else if (event is FilePartEvent) { - val filename: String = event.filename() - val contents: Flux = partEvents.map(PartEvent::content) - // handle file upload - } else { - return@switchOnFirst Mono.error(RuntimeException("Unexpected event: $event")) - } - } else { - return@switchOnFirst partEvents // either complete or error signal - } - } - } ----- -====== +include-code::./PartEventHandler[tag=snippet,indent=0] NOTE: The body contents of the `PartEvent` objects must be completely consumed, relayed, or released to avoid memory leaks. The following shows how to bind request parameters, URI variables, or headers via `DataBinder`, and also shows how to customize the `DataBinder`: -[tabs] -====== -Java:: -+ -[source,java] ----- -Pet pet = request.bind(Pet.class, dataBinder -> dataBinder.setAllowedFields("name")); ----- - -Kotlin:: -+ -[source,kotlin] ----- -val pet = request.bind(Pet::class.java) { dataBinder -> dataBinder.setAllowedFields("name") } ----- -====== - - +include-code::./RequestHandler[tag=snippet,indent=0] [[webflux-fn-response]] === ServerResponse @@ -325,24 +252,7 @@ a `build` method to create it. You can use the builder to set the response statu headers, or to provide a body. The following example creates a 200 (OK) response with JSON content: -[tabs] -====== -Java:: -+ -[source,java] ----- -Mono person = ... -ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class); ----- - -Kotlin:: -+ -[source,kotlin] ----- -val person: Mono = ... -ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person::class.java) ----- -====== +include-code::./ResponseHandler[tag=snippet,indent=0] The following example shows how to build a 201 (CREATED) response with a `Location` header and no body: @@ -353,7 +263,7 @@ Java:: [source,java] ---- URI location = ... -ServerResponse.created(location).build(); +return ServerResponse.created(location).build(); ---- Kotlin:: @@ -361,7 +271,7 @@ Kotlin:: [source,kotlin] ---- val location: URI = ... -ServerResponse.created(location).build() +return ServerResponse.created(location).build() ---- ====== @@ -374,14 +284,14 @@ Java:: + [source,java] ---- -ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...); +return ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...); ---- Kotlin:: + [source,kotlin] ---- -ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...) +return ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...) ---- ====== @@ -416,87 +326,7 @@ Therefore, it is useful to group related handler functions together into a handl has a similar role as `@Controller` in an annotation-based application. For example, the following class exposes a reactive `Person` repository: --- -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes"] ----- -import static org.springframework.http.MediaType.APPLICATION_JSON; -import static org.springframework.web.reactive.function.server.ServerResponse.ok; - -public class PersonHandler { - - private final PersonRepository repository; - - public PersonHandler(PersonRepository repository) { - this.repository = repository; - } - - public Mono listPeople(ServerRequest request) { // <1> - Flux people = repository.allPeople(); - return ok().contentType(APPLICATION_JSON).body(people, Person.class); - } - - public Mono createPerson(ServerRequest request) { // <2> - Mono person = request.bodyToMono(Person.class); - return ok().build(repository.savePerson(person)); - } - - public Mono getPerson(ServerRequest request) { // <3> - int personId = Integer.valueOf(request.pathVariable("id")); - return repository.getPerson(personId) - .flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person)) - .switchIfEmpty(ServerResponse.notFound().build()); - } -} ----- -<1> `listPeople` is a handler function that returns all `Person` objects found in the repository as -JSON. -<2> `createPerson` is a handler function that stores a new `Person` contained in the request body. -Note that `PersonRepository.savePerson(Person)` returns `Mono`: an empty `Mono` that emits -a completion signal when the person has been read from the request and stored. So we use the -`build(Publisher)` method to send a response when that completion signal is received (that is, -when the `Person` has been saved). -<3> `getPerson` is a handler function that returns a single person, identified by the `id` path -variable. We retrieve that `Person` from the repository and create a JSON response, if it is -found. If it is not found, we use `switchIfEmpty(Mono)` to return a 404 Not Found response. - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes"] ----- - class PersonHandler(private val repository: PersonRepository) { - - suspend fun listPeople(request: ServerRequest): ServerResponse { // <1> - val people: Flow = repository.allPeople() - return ok().contentType(APPLICATION_JSON).bodyAndAwait(people) - } - - suspend fun createPerson(request: ServerRequest): ServerResponse { // <2> - val person = request.awaitBody() - repository.savePerson(person) - return ok().buildAndAwait() - } - - suspend fun getPerson(request: ServerRequest): ServerResponse { // <3> - val personId = request.pathVariable("id").toInt() - return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).bodyValueAndAwait(it) } - ?: ServerResponse.notFound().buildAndAwait() - - } - } ----- -<1> `listPeople` is a handler function that returns all `Person` objects found in the repository as -JSON. -<2> `createPerson` is a handler function that stores a new `Person` contained in the request body. -Note that `PersonRepository.savePerson(Person)` is a suspending function with no return type. -<3> `getPerson` is a handler function that returns a single person, identified by the `id` path -variable. We retrieve that `Person` from the repository and create a JSON response, if it is -found. If it is not found, we return a 404 Not Found response. -====== --- +include-code::./PersonHandler[tag=snippet,indent=0] [[webflux-fn-handler-validation]] === Validation @@ -505,66 +335,7 @@ A functional endpoint can use Spring's xref:web/webmvc/mvc-config/validation.ado apply validation to the request body. For example, given a custom Spring xref:web/webmvc/mvc-config/validation.adoc[Validator] implementation for a `Person`: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes"] ----- - public class PersonHandler { - - private final Validator validator = new PersonValidator(); // <1> - - // ... - - public Mono createPerson(ServerRequest request) { - Mono person = request.bodyToMono(Person.class).doOnNext(this::validate); // <2> - return ok().build(repository.savePerson(person)); - } - - private void validate(Person person) { - Errors errors = new BeanPropertyBindingResult(person, "person"); - validator.validate(person, errors); - if (errors.hasErrors()) { - throw new ServerWebInputException(errors.toString()); // <3> - } - } - } ----- -<1> Create `Validator` instance. -<2> Apply validation. -<3> Raise exception for a 400 response. - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes"] ----- - class PersonHandler(private val repository: PersonRepository) { - - private val validator = PersonValidator() // <1> - - // ... - - suspend fun createPerson(request: ServerRequest): ServerResponse { - val person = request.awaitBody() - validate(person) // <2> - repository.savePerson(person) - return ok().buildAndAwait() - } - - private fun validate(person: Person) { - val errors: Errors = BeanPropertyBindingResult(person, "person") - validator.validate(person, errors) - if (errors.hasErrors()) { - throw ServerWebInputException(errors.toString()) // <3> - } - } - } ----- -<1> Create `Validator` instance. -<2> Apply validation. -<3> Raise exception for a 400 response. -====== +include-code::./PersonHandler[tag=snippet,indent=0] Handlers can also use the standard bean validation API (JSR-303) by creating and injecting a global `Validator` instance based on `LocalValidatorFactoryBean`. @@ -602,28 +373,7 @@ path, headers, xref:#api-version[API version], and more. The following example uses an `Accept` header, request predicate: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes"] ----- - RouterFunction route = RouterFunctions.route() - .GET("/hello-world", accept(MediaType.TEXT_PLAIN), - request -> ServerResponse.ok().bodyValue("Hello World")).build(); ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes"] ----- - val route = coRouter { - GET("/hello-world", accept(MediaType.TEXT_PLAIN)) { - ServerResponse.ok().bodyValueAndAwait("Hello World") - } - } ----- -====== +include-code::./RouterConfiguration[tag=snippet,indent=0] You can compose multiple request predicates together by using: @@ -658,61 +408,7 @@ There are also other ways to compose multiple router functions together: The following example shows the composition of four routes: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes"] ----- -import static org.springframework.http.MediaType.APPLICATION_JSON; -import static org.springframework.web.reactive.function.server.RequestPredicates.*; - -PersonRepository repository = ... -PersonHandler handler = new PersonHandler(repository); - -RouterFunction otherRoute = ... - -RouterFunction route = route() - .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) // <1> - .GET("/person", accept(APPLICATION_JSON), handler::listPeople) // <2> - .POST("/person", handler::createPerson) // <3> - .add(otherRoute) // <4> - .build(); ----- -<1> pass:q[`GET /person/{id}`] with an `Accept` header that matches JSON is routed to -`PersonHandler.getPerson` -<2> `GET /person` with an `Accept` header that matches JSON is routed to -`PersonHandler.listPeople` -<3> `POST /person` with no additional predicates is mapped to -`PersonHandler.createPerson`, and -<4> `otherRoute` is a router function that is created elsewhere, and added to the route built. - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes"] ----- - import org.springframework.http.MediaType.APPLICATION_JSON - - val repository: PersonRepository = ... - val handler = PersonHandler(repository) - - val otherRoute: RouterFunction = coRouter { } - - val route = coRouter { - GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) // <1> - GET("/person", accept(APPLICATION_JSON), handler::listPeople) // <2> - POST("/person", handler::createPerson) // <3> - }.and(otherRoute) // <4> ----- -<1> pass:q[`GET /person/{id}`] with an `Accept` header that matches JSON is routed to -`PersonHandler.getPerson` -<2> `GET /person` with an `Accept` header that matches JSON is routed to -`PersonHandler.listPeople` -<3> `POST /person` with no additional predicates is mapped to -`PersonHandler.createPerson`, and -<4> `otherRoute` is a router function that is created elsewhere, and added to the route built. -====== +include-code::./RouterConfiguration[tag=snippet,indent=0] [[nested-routes]] === Nested Routes @@ -1077,54 +773,7 @@ Now we can add a simple security filter to our route, assuming that we have a `S can determine whether a particular path is allowed. The following example shows how to do so: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes"] ----- - SecurityManager securityManager = ... - - RouterFunction route = route() - .path("/person", b1 -> b1 - .nest(accept(APPLICATION_JSON), b2 -> b2 - .GET("/{id}", handler::getPerson) - .GET(handler::listPeople)) - .POST(handler::createPerson)) - .filter((request, next) -> { - if (securityManager.allowAccessTo(request.path())) { - return next.handle(request); - } - else { - return ServerResponse.status(UNAUTHORIZED).build(); - } - }) - .build(); ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes"] ----- - val securityManager: SecurityManager = ... - - val route = router { - ("/person" and accept(APPLICATION_JSON)).nest { - GET("/{id}", handler::getPerson) - GET(handler::listPeople) - POST(handler::createPerson) - filter { request, next -> - if (securityManager.allowAccessTo(request.path())) { - next(request) - } - else { - status(UNAUTHORIZED).build() - } - } - } - } ----- -====== +include-code::./RouterConfiguration[tag=snippet,indent=0] The preceding example demonstrates that invoking the `next.handle(ServerRequest)` is optional. We only let the handler function be run when access is allowed. diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerclasses/Person.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerclasses/Person.java new file mode 100644 index 00000000000..9a5e7fea9b8 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerclasses/Person.java @@ -0,0 +1,19 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnhandlerclasses; + +public record Person(String name) { } diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerclasses/PersonHandler.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerclasses/PersonHandler.java new file mode 100644 index 00000000000..e8b00303b33 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerclasses/PersonHandler.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnhandlerclasses; + +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.web.reactive.function.server.ServerResponse.ok; + +// tag::snippet[] +public class PersonHandler { + + private final PersonRepository repository; + + public PersonHandler(PersonRepository repository) { + this.repository = repository; + } + + // listPeople is a handler function that returns all Person objects found + // in the repository as JSON + public Mono listPeople(ServerRequest request) { + Flux people = repository.allPeople(); + return ok().contentType(APPLICATION_JSON).body(people, Person.class); + } + + // createPerson is a handler function that stores a new Person contained + // in the request body. + // Note that PersonRepository.savePerson(Person) returns Mono: an empty + // Mono that emits a completion signal when the person has been read from the + // request and stored. So we use the build(Publisher) method to send a + // response when that completion signal is received (that is, when the Person + // has been saved) + public Mono createPerson(ServerRequest request) { + Mono person = request.bodyToMono(Person.class); + return ok().build(repository.savePerson(person)); + } + + // getPerson is a handler function that returns a single person, identified by + // the id path variable. We retrieve that Person from the repository and create + // a JSON response, if it is found. If it is not found, we use switchIfEmpty(Mono) + // to return a 404 Not Found response. + public Mono getPerson(ServerRequest request) { + int personId = Integer.valueOf(request.pathVariable("id")); + return repository.getPerson(personId) + .flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person)) + .switchIfEmpty(ServerResponse.notFound().build()); + } +} +// end::snippet[] + diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerclasses/PersonRepository.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerclasses/PersonRepository.java new file mode 100644 index 00000000000..34372f19847 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerclasses/PersonRepository.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnhandlerclasses; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public interface PersonRepository { + + Flux allPeople(); + + Mono savePerson(Mono person); + + Mono getPerson(int id); +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerfilterfunction/RouterConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerfilterfunction/RouterConfiguration.java new file mode 100644 index 00000000000..4ff62dbcd41 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerfilterfunction/RouterConfiguration.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnhandlerfilterfunction; + +import org.springframework.docs.web.webfluxfnhandlerclasses.PersonHandler; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; + +import static org.springframework.http.HttpStatus.UNAUTHORIZED; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.web.reactive.function.server.RequestPredicates.accept; + +public class RouterConfiguration { + + public RouterFunction route(PersonHandler handler) { + // tag::snippet[] + SecurityManager securityManager = getSecurityManager(); + + RouterFunction route = RouterFunctions.route() + .path("/person", b1 -> b1 + .nest(accept(APPLICATION_JSON), b2 -> b2 + .GET("/{id}", handler::getPerson) + .GET(handler::listPeople)) + .POST(handler::createPerson)) + .filter((request, next) -> { + if (securityManager.allowAccessTo(request.path())) { + return next.handle(request); + } + else { + return ServerResponse.status(UNAUTHORIZED).build(); + } + }).build(); + // end::snippet[] + return route; + } + + SecurityManager getSecurityManager() { + return path -> false; + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerfilterfunction/SecurityManager.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerfilterfunction/SecurityManager.java new file mode 100644 index 00000000000..b2b24b435ef --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlerfilterfunction/SecurityManager.java @@ -0,0 +1,22 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnhandlerfilterfunction; + +public interface SecurityManager { + + boolean allowAccessTo(String path); +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlervalidation/PersonHandler.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlervalidation/PersonHandler.java new file mode 100644 index 00000000000..849c7b8e06e --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlervalidation/PersonHandler.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnhandlervalidation; + +import org.springframework.docs.web.webfluxfnhandlerclasses.Person; +import org.springframework.docs.web.webfluxfnhandlerclasses.PersonRepository; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.ServerWebInputException; +import reactor.core.publisher.Mono; + +import static org.springframework.web.reactive.function.server.ServerResponse.ok; + +// tag::snippet[] +public class PersonHandler { + + // Create Validator instance + private final Validator validator = new PersonValidator(); + + private final PersonRepository repository; + + public PersonHandler(PersonRepository repository) { + this.repository = repository; + } + + public Mono createPerson(ServerRequest request) { + // Apply validation + Mono person = request.bodyToMono(Person.class).doOnNext(this::validate); + return ok().build(repository.savePerson(person)); + } + + private void validate(Person person) { + Errors errors = new BeanPropertyBindingResult(person, "person"); + validator.validate(person, errors); + if (errors.hasErrors()) { + // Raise exception for a 400 response + throw new ServerWebInputException(errors.toString()); + } + } +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlervalidation/PersonValidator.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlervalidation/PersonValidator.java new file mode 100644 index 00000000000..0ea8f849464 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnhandlervalidation/PersonValidator.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnhandlervalidation; + +import org.springframework.docs.web.webfluxfnhandlerclasses.Person; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +public class PersonValidator implements Validator { + + @Override + public boolean supports(Class clazz) { + return Person.class.isAssignableFrom(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + // Validation logic + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnpredicates/RouterConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnpredicates/RouterConfiguration.java new file mode 100644 index 00000000000..42c8aac28ec --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnpredicates/RouterConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnpredicates; + +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; + +import static org.springframework.web.reactive.function.server.RequestPredicates.accept; + +public class RouterConfiguration { + + public RouterFunction route() { + // tag::snippet[] + RouterFunction route = RouterFunctions.route() + .GET("/hello-world", accept(MediaType.TEXT_PLAIN), + request -> ServerResponse.ok().bodyValue("Hello World")).build(); + // end::snippet[] + return route; + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnrequest/PartEventHandler.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnrequest/PartEventHandler.java new file mode 100644 index 00000000000..23b7f23d323 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnrequest/PartEventHandler.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnrequest; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.codec.multipart.FilePartEvent; +import org.springframework.http.codec.multipart.FormPartEvent; +import org.springframework.http.codec.multipart.PartEvent; +import org.springframework.web.reactive.function.server.ServerRequest; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class PartEventHandler { + + public void handle(ServerRequest request) { + // tag::snippet[] + request.bodyToFlux(PartEvent.class).windowUntil(PartEvent::isLast) + .concatMap(p -> p.switchOnFirst((signal, partEvents) -> { + if (signal.hasValue()) { + PartEvent event = signal.get(); + if (event instanceof FormPartEvent formEvent) { + String value = formEvent.value(); + // handle form field + } + else if (event instanceof FilePartEvent fileEvent) { + String filename = fileEvent.filename(); + Flux contents = partEvents.map(PartEvent::content); + // handle file upload + } + else { + return Mono.error(new RuntimeException("Unexpected event: " + event)); + } + } + else { + return partEvents; // either complete or error signal + } + return Mono.empty(); + })); + // end::snippet[] + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnrequest/RequestHandler.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnrequest/RequestHandler.java new file mode 100644 index 00000000000..b23e6590bba --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnrequest/RequestHandler.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnrequest; + +import reactor.core.publisher.Mono; + +import org.springframework.web.reactive.function.server.ServerRequest; + +public class RequestHandler { + + public void bind(ServerRequest request) { + // tag::snippet[] + Mono pet = request.bind(Pet.class, dataBinder -> dataBinder.setAllowedFields("name")); + // end::snippet[] + } + + record Pet(String name) { } + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnresponse/ResponseHandler.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnresponse/ResponseHandler.java new file mode 100644 index 00000000000..37914babb12 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnresponse/ResponseHandler.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnresponse; + +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; + +public class ResponseHandler { + + public Mono createResponse() { + // tag::snippet[] + Mono person = getPerson(); + return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class); + // end::snippet[] + } + + private Mono getPerson() { + return Mono.just(new Person("foo")); + } + + record Person(String name) { } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnroutes/RouterConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnroutes/RouterConfiguration.java new file mode 100644 index 00000000000..9e88ea2766c --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webfluxfnroutes/RouterConfiguration.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnroutes; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.docs.web.webfluxfnhandlerclasses.Person; +import org.springframework.docs.web.webfluxfnhandlerclasses.PersonHandler; +import org.springframework.docs.web.webfluxfnhandlerclasses.PersonRepository; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; + +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.web.reactive.function.server.RequestPredicates.accept; +import static org.springframework.web.reactive.function.server.RouterFunctions.route; + +public class RouterConfiguration { + + public RouterFunction routes() { + // tag::snippet[] + PersonRepository repository = getPersonRepository(); + PersonHandler handler = new PersonHandler(repository); + + RouterFunction otherRoute = getOtherRoute(); + + RouterFunction route = route() + // GET /person/{id} with an Accept header that matches JSON is routed to PersonHandler.getPerson + .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) + // GET /person with an Accept header that matches JSON is routed to PersonHandler.listPeople + .GET("/person", accept(APPLICATION_JSON), handler::listPeople) + // POST /person with no additional predicates is mapped to PersonHandler.createPerson + .POST("/person", handler::createPerson) + // otherRoute is a router function that is created elsewhere and added to the route built + .add(otherRoute) + .build(); + // end::snippet[] + return route; + } + + PersonRepository getPersonRepository() { + return new PersonRepository() { + @Override + public Flux allPeople() { + return null; + } + + @Override + public Mono savePerson(Mono person) { + return null; + } + + @Override + public Mono getPerson(int id) { + return null; + } + }; + } + + RouterFunction getOtherRoute() { + return RouterFunctions.route().build(); + } +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerclasses/Person.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerclasses/Person.kt new file mode 100644 index 00000000000..c2336a54c6b --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerclasses/Person.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnhandlerclasses + +data class Person(val name: String) diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerclasses/PersonHandler.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerclasses/PersonHandler.kt new file mode 100644 index 00000000000..7554aa5a47f --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerclasses/PersonHandler.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnhandlerclasses + +import kotlinx.coroutines.flow.Flow +import org.springframework.http.MediaType.APPLICATION_JSON +import org.springframework.web.reactive.function.server.ServerRequest +import org.springframework.web.reactive.function.server.ServerResponse +import org.springframework.web.reactive.function.server.awaitBody +import org.springframework.web.reactive.function.server.bodyAndAwait +import org.springframework.web.reactive.function.server.bodyValueAndAwait +import org.springframework.web.reactive.function.server.buildAndAwait + +// tag::snippet[] +class PersonHandler(private val repository: PersonRepository) { + + // listPeople is a handler function that returns all Person objects found + // in the repository as JSON + suspend fun listPeople(request: ServerRequest): ServerResponse { + val people: Flow = repository.allPeople() + return ServerResponse.ok().contentType(APPLICATION_JSON).bodyAndAwait(people) + } + + // createPerson is a handler function that stores a new Person contained + // in the request body. + // Note that PersonRepository.savePerson(Person) returns Mono: an empty + // Mono that emits a completion signal when the person has been read from the + // request and stored. So we use the build(Publisher) method to send a + // response when that completion signal is received (that is, when the Person + // has been saved) + suspend fun createPerson(request: ServerRequest): ServerResponse { + val person = request.awaitBody() + repository.savePerson(person) + return ServerResponse.ok().buildAndAwait() + } + + // getPerson is a handler function that returns a single person, identified by + // the id path variable. We retrieve that Person from the repository and create + // a JSON response, if it is found. If it is not found, we use switchIfEmpty(Mono) + // to return a 404 Not Found response. + suspend fun getPerson(request: ServerRequest): ServerResponse { + val personId = request.pathVariable("id").toInt() + return repository.getPerson(personId)?.let { ServerResponse.ok().contentType(APPLICATION_JSON).bodyValueAndAwait(it) } + ?: ServerResponse.notFound().buildAndAwait() + + } +} +// end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerclasses/PersonRepository.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerclasses/PersonRepository.kt new file mode 100644 index 00000000000..13963494c76 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerclasses/PersonRepository.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnhandlerclasses + +import kotlinx.coroutines.flow.Flow + +interface PersonRepository { + + fun allPeople(): Flow + + suspend fun savePerson(person: Person) + + suspend fun getPerson(id: Int): Person? +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerfilterfunction/RouterConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerfilterfunction/RouterConfiguration.kt new file mode 100644 index 00000000000..a9648256285 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerfilterfunction/RouterConfiguration.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnhandlerfilterfunction + +import org.springframework.docs.web.webfluxfnhandlerclasses.PersonHandler +import org.springframework.http.HttpStatus.UNAUTHORIZED +import org.springframework.http.MediaType.APPLICATION_JSON +import org.springframework.web.reactive.function.server.RouterFunction +import org.springframework.web.reactive.function.server.ServerResponse +import org.springframework.web.reactive.function.server.buildAndAwait +import org.springframework.web.reactive.function.server.coRouter + +class RouterConfiguration { + + fun route(handler: PersonHandler): RouterFunction { + // tag::snippet[] + val securityManager: SecurityManager = getSecurityManager() + + val route = coRouter { + ("/person" and accept(APPLICATION_JSON)).nest { + GET("/{id}", handler::getPerson) + GET("/", handler::listPeople) + POST("/", handler::createPerson) + filter { request, next -> + if (securityManager.allowAccessTo(request.path())) { + next(request) + } + else { + ServerResponse.status(UNAUTHORIZED).buildAndAwait() + } + } + } + } + // end::snippet[] + return route + } + +} + +fun getSecurityManager() = object : SecurityManager { + override fun allowAccessTo(path: String): Boolean { + TODO("Not yet implemented") + } +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerfilterfunction/SecurityManager.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerfilterfunction/SecurityManager.kt new file mode 100644 index 00000000000..2e2025a854e --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlerfilterfunction/SecurityManager.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnhandlerfilterfunction + +interface SecurityManager { + + fun allowAccessTo(path: String): Boolean +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlervalidation/PersonHandler.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlervalidation/PersonHandler.kt new file mode 100644 index 00000000000..206a6ad683b --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlervalidation/PersonHandler.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnhandlervalidation + +import org.springframework.docs.web.webfluxfnhandlerclasses.Person +import org.springframework.docs.web.webfluxfnhandlerclasses.PersonRepository +import org.springframework.validation.BeanPropertyBindingResult +import org.springframework.validation.Errors +import org.springframework.web.reactive.function.server.ServerRequest +import org.springframework.web.reactive.function.server.ServerResponse +import org.springframework.web.reactive.function.server.awaitBody +import org.springframework.web.reactive.function.server.buildAndAwait +import org.springframework.web.server.ServerWebInputException + +// tag::snippet[] +class PersonHandler(private val repository: PersonRepository) { + + // Create Validator instance + private val validator = PersonValidator() + + suspend fun createPerson(request: ServerRequest): ServerResponse { + val person = request.awaitBody() + // Apply validation + validate(person) + repository.savePerson(person) + return ServerResponse.ok().buildAndAwait() + } + + private fun validate(person: Person) { + val errors: Errors = BeanPropertyBindingResult(person, "person") + validator.validate(person, errors) + if (errors.hasErrors()) { + // Raise exception for a 400 response + throw ServerWebInputException(errors.toString()) + } + } +} +// end::snippet[] + diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlervalidation/PersonValidator.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlervalidation/PersonValidator.kt new file mode 100644 index 00000000000..e5a7dd5112f --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnhandlervalidation/PersonValidator.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnhandlervalidation + +import org.springframework.docs.web.webfluxfnhandlerclasses.Person +import org.springframework.validation.Errors +import org.springframework.validation.Validator + +class PersonValidator : Validator { + + override fun supports(clazz: Class<*>): Boolean { + return Person::class.java.isAssignableFrom(clazz) + } + + override fun validate(target: Any, errors: Errors) { + // Validation logic + } +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnpredicates/RouterConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnpredicates/RouterConfiguration.kt new file mode 100644 index 00000000000..76d93b1444b --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnpredicates/RouterConfiguration.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnpredicates + +import org.springframework.http.MediaType +import org.springframework.web.reactive.function.server.RouterFunction +import org.springframework.web.reactive.function.server.ServerResponse +import org.springframework.web.reactive.function.server.bodyValueAndAwait +import org.springframework.web.reactive.function.server.coRouter + +class RouterConfiguration { + + fun route(): RouterFunction { + // tag::snippet[] + val route = coRouter { + GET("/hello-world", accept(MediaType.TEXT_PLAIN)) { + ServerResponse.ok().bodyValueAndAwait("Hello World") + } + } + // end::snippet[] + return route + } +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnrequest/PartEventHandler.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnrequest/PartEventHandler.kt new file mode 100644 index 00000000000..c9ac0e0d424 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnrequest/PartEventHandler.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnrequest + +import org.springframework.core.io.buffer.DataBuffer +import org.springframework.http.codec.multipart.FilePartEvent +import org.springframework.http.codec.multipart.FormPartEvent +import org.springframework.http.codec.multipart.PartEvent +import org.springframework.web.reactive.function.server.ServerRequest +import org.springframework.web.reactive.function.server.bodyToFlux +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono + +class PartEventHandler { + + fun handle(request: ServerRequest) { + // tag::snippet[] + request.bodyToFlux().windowUntil(PartEvent::isLast) + .concatMap { + it.switchOnFirst { signal, partEvents -> + if (signal.hasValue()) { + val event = signal.get() + if (event is FormPartEvent) { + val value: String = event.value() + // handle form field + } else if (event is FilePartEvent) { + val filename: String = event.filename() + val contents: Flux = partEvents.map(PartEvent::content) + // handle file upload + } else { + return@switchOnFirst Mono.error(RuntimeException("Unexpected event: $event")) + } + } else { + return@switchOnFirst partEvents // either complete or error signal + } + Mono.empty() + } + } + // end::snippet[] + } +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnrequest/RequestHandler.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnrequest/RequestHandler.kt new file mode 100644 index 00000000000..c2a1fc1dfd2 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnrequest/RequestHandler.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnrequest + +import org.springframework.web.reactive.function.server.ServerRequest +import reactor.core.publisher.Mono + +class RequestHandler { + + fun bind(request: ServerRequest) { + // tag::snippet[] + val pet: Mono = request.bind(Pet::class.java) { dataBinder -> dataBinder.setAllowedFields("name") } + // end::snippet[] + } + + data class Pet(val name: String) +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnresponse/ResponseHandler.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnresponse/ResponseHandler.kt new file mode 100644 index 00000000000..9fd0ad27fc1 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnresponse/ResponseHandler.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnresponse + +import org.springframework.http.MediaType +import org.springframework.web.reactive.function.server.ServerResponse +import org.springframework.web.reactive.function.server.bodyValueWithTypeAndAwait + +class ResponseHandler { + + suspend fun createResponse(): ServerResponse { + // tag::snippet[] + val person: Person = getPerson() + return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValueWithTypeAndAwait(person) + // end::snippet[] + } + + fun getPerson() = Person("foo") + + data class Person(val name: String) +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnroutes/RouterConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnroutes/RouterConfiguration.kt new file mode 100644 index 00000000000..f958f21e32c --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webfluxfnroutes/RouterConfiguration.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webfluxfnroutes + +import kotlinx.coroutines.flow.Flow +import org.springframework.docs.web.webfluxfnhandlerclasses.Person +import org.springframework.docs.web.webfluxfnhandlerclasses.PersonHandler +import org.springframework.docs.web.webfluxfnhandlerclasses.PersonRepository +import org.springframework.http.MediaType.APPLICATION_JSON +import org.springframework.web.reactive.function.server.RouterFunction +import org.springframework.web.reactive.function.server.ServerResponse +import org.springframework.web.reactive.function.server.coRouter + +class RouterConfiguration { + + fun routes(): RouterFunction { + // tag::snippet[] + val repository: PersonRepository = getPersonRepository() + val handler = PersonHandler(repository) + + val otherRoute: RouterFunction = getOtherRoute() + + val route = coRouter { + // GET /person/{id} with an Accept header that matches JSON is routed to PersonHandler.getPerson + GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) + // GET /person with an Accept header that matches JSON is routed to PersonHandler.listPeople + GET("/person", accept(APPLICATION_JSON), handler::listPeople) + // POST /person with no additional predicates is mapped to PersonHandler.createPerson + POST("/person", handler::createPerson) + // otherRoute is a router function that is created elsewhere and added to the route built + }.and(otherRoute) + // end::snippet[] + return route + } +} + +fun getOtherRoute() = coRouter { } + +fun getPersonRepository() = object: PersonRepository { + override fun allPeople(): Flow { + TODO("Not yet implemented") + } + + override suspend fun savePerson(person: Person) { + TODO("Not yet implemented") + } + + override suspend fun getPerson(id: Int): Person? { + TODO("Not yet implemented") + } +}