diff --git a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt new file mode 100644 index 0000000000..e314fc4810 --- /dev/null +++ b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt @@ -0,0 +1,508 @@ +/* + * Copyright 2002-2019 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.web.servlet.function + +import org.springframework.core.io.Resource +import org.springframework.http.HttpMethod +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import java.net.URI +import java.util.* +import java.util.function.Supplier + +/** + * Allow to create easily a WebMvc.fn `RouterFunction` from a Kotlin router + * DSL leveraging WebMvc.fn the Java API ([RouterFunction], [RequestPredicate], + * [HandlerFunction]). + * + * Example: + * + * ``` + * @Configuration + * class ApplicationRoutes(val userHandler: UserHandler) { + * + * @Bean + * fun mainRouter() = router { + * accept(TEXT_HTML).nest { + * (GET("/user/") or GET("/users/")).invoke(userHandler::findAllView) + * GET("/users/{login}", userHandler::findViewById) + * } + * accept(APPLICATION_JSON).nest { + * (GET("/api/user/") or GET("/api/users/")).invoke(userHandler::findAll) + * POST("/api/users/", userHandler::create) + * } + * } + * + * } + * ``` + * @author Sebastien Deleuze + * @see RouterFunctionDsl + * @see RouterFunctions.Builder + * @since 5.2 + */ +fun router(routes: (RouterFunctionDsl.() -> Unit)) = RouterFunctionDsl(routes).build() + +/** + * Provide a WebMvc.fn [RouterFunction] Kotlin DSL in order to be able to write idiomatic Kotlin code. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +class RouterFunctionDsl(private val init: (RouterFunctionDsl.() -> Unit)) { + + private val builder = RouterFunctions.route() + + /** + * Return a composed request predicate that tests against both this predicate AND + * the [other] predicate (String processed as a path predicate). When evaluating the + * composed predicate, if this predicate is `false`, then the [other] predicate is not + * evaluated. + * @see RequestPredicate.and + * @see RequestPredicates.path + */ + infix fun RequestPredicate.and(other: String): RequestPredicate = this.and(path(other)) + + /** + * Return a composed request predicate that tests against both this predicate OR + * the [other] predicate (String processed as a path predicate). When evaluating the + * composed predicate, if this predicate is `true`, then the [other] predicate is not + * evaluated. + * @see RequestPredicate.or + * @see RequestPredicates.path + */ + infix fun RequestPredicate.or(other: String): RequestPredicate = this.or(path(other)) + + /** + * Return a composed request predicate that tests against both this predicate (String + * processed as a path predicate) AND the [other] predicate. When evaluating the + * composed predicate, if this predicate is `false`, then the [other] predicate is not + * evaluated. + * @see RequestPredicate.and + * @see RequestPredicates.path + */ + infix fun String.and(other: RequestPredicate): RequestPredicate = path(this).and(other) + + /** + * Return a composed request predicate that tests against both this predicate (String + * processed as a path predicate) OR the [other] predicate. When evaluating the + * composed predicate, if this predicate is `true`, then the [other] predicate is not + * evaluated. + * @see RequestPredicate.or + * @see RequestPredicates.path + */ + infix fun String.or(other: RequestPredicate): RequestPredicate = path(this).or(other) + + /** + * Return a composed request predicate that tests against both this predicate AND + * the [other] predicate. When evaluating the composed predicate, if this + * predicate is `false`, then the [other] predicate is not evaluated. + * @see RequestPredicate.and + */ + infix fun RequestPredicate.and(other: RequestPredicate): RequestPredicate = this.and(other) + + /** + * Return a composed request predicate that tests against both this predicate OR + * the [other] predicate. When evaluating the composed predicate, if this + * predicate is `true`, then the [other] predicate is not evaluated. + * @see RequestPredicate.or + */ + infix fun RequestPredicate.or(other: RequestPredicate): RequestPredicate = this.or(other) + + /** + * Return a predicate that represents the logical negation of this predicate. + */ + operator fun RequestPredicate.not(): RequestPredicate = this.negate() + + /** + * Route to the given router function if the given request predicate applies. This + * method can be used to create *nested routes*, where a group of routes share a + * common path (prefix), header, or other request predicate. + * @see RouterFunctions.nest + */ + fun RequestPredicate.nest(r: (RouterFunctionDsl.() -> Unit)) { + builder.nest(this, Supplier(RouterFunctionDsl(r)::build)) + } + + + /** + * Route to the given router function if the given request predicate (String + * processed as a path predicate) applies. This method can be used to create + * *nested routes*, where a group of routes share a common path + * (prefix), header, or other request predicate. + * @see RouterFunctions.nest + * @see RequestPredicates.path + */ + fun String.nest(r: (RouterFunctionDsl.() -> Unit)) = path(this).nest(r) + + /** + * Route to the given handler function if the given request predicate applies. + * @see RouterFunctions.route + */ + fun GET(pattern: String, f: (ServerRequest) -> ServerResponse) { + builder.GET(pattern, HandlerFunction(f)) + } + + /** + * Return a [RequestPredicate] that matches if request's HTTP method is `GET` + * and the given `pattern` matches against the request path. + * @see RequestPredicates.GET + */ + fun GET(pattern: String): RequestPredicate = RequestPredicates.GET(pattern) + + /** + * Route to the given handler function if the given request predicate applies. + * @see RouterFunctions.route + */ + fun HEAD(pattern: String, f: (ServerRequest) -> ServerResponse) { + builder.HEAD(pattern, HandlerFunction(f)) + } + + /** + * Return a [RequestPredicate] that matches if request's HTTP method is `HEAD` + * and the given `pattern` matches against the request path. + * @see RequestPredicates.HEAD + */ + fun HEAD(pattern: String): RequestPredicate = RequestPredicates.HEAD(pattern) + + /** + * Route to the given handler function if the given `POST` predicate applies. + * @see RouterFunctions.route + */ + fun POST(pattern: String, f: (ServerRequest) -> ServerResponse) { + builder.POST(pattern, HandlerFunction(f)) + } + + /** + * Return a [RequestPredicate] that matches if request's HTTP method is `POST` + * and the given `pattern` matches against the request path. + * @see RequestPredicates.POST + */ + fun POST(pattern: String): RequestPredicate = RequestPredicates.POST(pattern) + + /** + * Route to the given handler function if the given `PUT` predicate applies. + * @see RouterFunctions.route + */ + fun PUT(pattern: String, f: (ServerRequest) -> ServerResponse) { + builder.PUT(pattern, HandlerFunction(f)) + } + + /** + * Return a [RequestPredicate] that matches if request's HTTP method is `PUT` + * and the given `pattern` matches against the request path. + * @see RequestPredicates.PUT + */ + fun PUT(pattern: String): RequestPredicate = RequestPredicates.PUT(pattern) + + /** + * Route to the given handler function if the given `PATCH` predicate applies. + * @see RouterFunctions.route + */ + fun PATCH(pattern: String, f: (ServerRequest) -> ServerResponse) { + builder.PATCH(pattern, HandlerFunction(f)) + } + + /** + * Return a [RequestPredicate] that matches if request's HTTP method is `PATCH` + * and the given `pattern` matches against the request path. + * @param pattern the path pattern to match against + * @return a predicate that matches if the request method is `PATCH` and if the given pattern + * matches against the request path + */ + fun PATCH(pattern: String): RequestPredicate = RequestPredicates.PATCH(pattern) + + /** + * Route to the given handler function if the given `DELETE` predicate applies. + * @see RouterFunctions.route + */ + fun DELETE(pattern: String, f: (ServerRequest) -> ServerResponse) { + builder.DELETE(pattern, HandlerFunction(f)) + } + + /** + * Return a [RequestPredicate] that matches if request's HTTP method is `DELETE` + * and the given `pattern` matches against the request path. + * @param pattern the path pattern to match against + * @return a predicate that matches if the request method is `DELETE` and if the given pattern + * matches against the request path + */ + fun DELETE(pattern: String): RequestPredicate = RequestPredicates.DELETE(pattern) + + /** + * Route to the given handler function if the given OPTIONS predicate applies. + * @see RouterFunctions.route + */ + fun OPTIONS(pattern: String, f: (ServerRequest) -> ServerResponse) { + builder.OPTIONS(pattern, HandlerFunction(f)) + } + + /** + * Return a [RequestPredicate] that matches if request's HTTP method is `OPTIONS` + * and the given `pattern` matches against the request path. + * @param pattern the path pattern to match against + * @return a predicate that matches if the request method is `OPTIONS` and if the given pattern + * matches against the request path + */ + fun OPTIONS(pattern: String): RequestPredicate = RequestPredicates.OPTIONS(pattern) + + /** + * Route to the given handler function if the given accept predicate applies. + * @see RouterFunctions.route + */ + fun accept(mediaType: MediaType, f: (ServerRequest) -> ServerResponse) { + builder.add(RouterFunctions.route(RequestPredicates.accept(mediaType), HandlerFunction(f))) + } + + /** + * Return a [RequestPredicate] that tests if the request's + * [accept][ServerRequest.Headers.accept] } header is + * [compatible][MediaType.isCompatibleWith] with any of the given media types. + * @param mediaType the media types to match the request's accept header against + * @return a predicate that tests the request's accept header against the given media types + */ + fun accept(vararg mediaType: MediaType): RequestPredicate = RequestPredicates.accept(*mediaType) + + /** + * Route to the given handler function if the given contentType predicate applies. + * @see RouterFunctions.route + */ + fun contentType(mediaType: MediaType, f: (ServerRequest) -> ServerResponse) { + builder.add(RouterFunctions.route(RequestPredicates.contentType(mediaType), HandlerFunction(f))) + } + + /** + * Return a [RequestPredicate] that tests if the request's + * [content type][ServerRequest.Headers.contentType] is + * [included][MediaType.includes] by any of the given media types. + * @param mediaTypes the media types to match the request's content type against + * @return a predicate that tests the request's content type against the given media types + */ + fun contentType(vararg mediaTypes: MediaType): RequestPredicate = RequestPredicates.contentType(*mediaTypes) + + /** + * Route to the given handler function if the given headers predicate applies. + * @see RouterFunctions.route + */ + fun headers(headersPredicate: (ServerRequest.Headers) -> Boolean, f: (ServerRequest) -> ServerResponse) { + builder.add(RouterFunctions.route(RequestPredicates.headers(headersPredicate), HandlerFunction(f))) + } + + /** + * Return a [RequestPredicate] that tests the request's headers against the given headers predicate. + * @param headersPredicate a predicate that tests against the request headers + * @return a predicate that tests against the given header predicate + */ + fun headers(headersPredicate: (ServerRequest.Headers) -> Boolean): RequestPredicate = + RequestPredicates.headers(headersPredicate) + + /** + * Route to the given handler function if the given method predicate applies. + * @see RouterFunctions.route + */ + fun method(httpMethod: HttpMethod, f: (ServerRequest) -> ServerResponse) { + builder.add(RouterFunctions.route(RequestPredicates.method(httpMethod), HandlerFunction(f))) + } + + /** + * Return a [RequestPredicate] that tests against the given HTTP method. + * @param httpMethod the HTTP method to match to + * @return a predicate that tests against the given HTTP method + */ + fun method(httpMethod: HttpMethod): RequestPredicate = RequestPredicates.method(httpMethod) + + /** + * Route to the given handler function if the given path predicate applies. + * @see RouterFunctions.route + */ + fun path(pattern: String, f: (ServerRequest) -> ServerResponse) { + builder.add(RouterFunctions.route(RequestPredicates.path(pattern), HandlerFunction(f))) + } + + /** + * Return a [RequestPredicate] that tests the request path against the given path pattern. + * @see RequestPredicates.path + */ + fun path(pattern: String): RequestPredicate = RequestPredicates.path(pattern) + + /** + * Route to the given handler function if the given pathExtension predicate applies. + * @see RouterFunctions.route + */ + fun pathExtension(extension: String, f: (ServerRequest) -> ServerResponse) { + builder.add(RouterFunctions.route(RequestPredicates.pathExtension(extension), HandlerFunction(f))) + } + + /** + * Return a [RequestPredicate] that matches if the request's path has the given extension. + * @param extension the path extension to match against, ignoring case + * @return a predicate that matches if the request's path has the given file extension + */ + fun pathExtension(extension: String): RequestPredicate = RequestPredicates.pathExtension(extension) + + /** + * Route to the given handler function if the given pathExtension predicate applies. + * @see RouterFunctions.route + */ + fun pathExtension(predicate: (String) -> Boolean, f: (ServerRequest) -> ServerResponse) { + builder.add(RouterFunctions.route(RequestPredicates.pathExtension(predicate), HandlerFunction(f))) + } + + /** + * Return a [RequestPredicate] that matches if the request's path matches the given + * predicate. + * @see RequestPredicates.pathExtension + */ + fun pathExtension(predicate: (String) -> Boolean): RequestPredicate = + RequestPredicates.pathExtension(predicate) + + /** + * Route to the given handler function if the given queryParam predicate applies. + * @see RouterFunctions.route + */ + fun param(name: String, predicate: (String) -> Boolean, f: (ServerRequest) -> ServerResponse) { + builder.add(RouterFunctions.route(RequestPredicates.param(name, predicate), HandlerFunction(f))) + } + + /** + * Return a [RequestPredicate] that tests the request's query parameter of the given name + * against the given predicate. + * @param name the name of the query parameter to test against + * @param predicate predicate to test against the query parameter value + * @return a predicate that matches the given predicate against the query parameter of the given name + * @see ServerRequest#queryParam + */ + fun param(name: String, predicate: (String) -> Boolean): RequestPredicate = + RequestPredicates.param(name, predicate) + + /** + * Route to the given handler function if the given request predicate applies. + * @see RouterFunctions.route + */ + operator fun RequestPredicate.invoke(f: (ServerRequest) -> ServerResponse) { + builder.add(RouterFunctions.route(this, HandlerFunction(f))) + } + + /** + * Route to the given handler function if the given predicate (String + * processed as a path predicate) applies. + * @see RouterFunctions.route + */ + operator fun String.invoke(f: (ServerRequest) -> ServerResponse) { + builder.add(RouterFunctions.route(RequestPredicates.path(this), HandlerFunction(f))) + } + + /** + * Route requests that match the given pattern to resources relative to the given root location. + * @see RouterFunctions.resources + */ + fun resources(path: String, location: Resource) { + builder.resources(path, location) + } + + /** + * Route to resources using the provided lookup function. If the lookup function provides a + * [Resource] for the given request, it will be it will be exposed using a + * [HandlerFunction] that handles GET, HEAD, and OPTIONS requests. + */ + fun resources(lookupFunction: (ServerRequest) -> Resource?) { + builder.resources { + Optional.ofNullable(lookupFunction.invoke(it)) + } + } + + /** + * Return a composed routing function created from all the registered routes. + */ + internal fun build(): RouterFunction { + init() + return builder.build() + } + + /** + * @see ServerResponse.from + */ + fun from(other: ServerResponse) = + ServerResponse.from(other) + + /** + * @see ServerResponse.created + */ + fun created(location: URI) = + ServerResponse.created(location) + + /** + * @see ServerResponse.ok + */ + fun ok() = ServerResponse.ok() + + /** + * @see ServerResponse.noContent + */ + fun noContent() = ServerResponse.noContent() + + /** + * @see ServerResponse.accepted + */ + fun accepted() = ServerResponse.accepted() + + /** + * @see ServerResponse.permanentRedirect + */ + fun permanentRedirect(location: URI) = ServerResponse.permanentRedirect(location) + + /** + * @see ServerResponse.temporaryRedirect + */ + fun temporaryRedirect(location: URI) = ServerResponse.temporaryRedirect(location) + + /** + * @see ServerResponse.seeOther + */ + fun seeOther(location: URI) = ServerResponse.seeOther(location) + + /** + * @see ServerResponse.badRequest + */ + fun badRequest() = ServerResponse.badRequest() + + /** + * @see ServerResponse.notFound + */ + fun notFound() = ServerResponse.notFound() + + /** + * @see ServerResponse.unprocessableEntity + */ + fun unprocessableEntity() = ServerResponse.unprocessableEntity() + + /** + * @see ServerResponse.status + */ + fun status(status: HttpStatus) = ServerResponse.status(status) + + /** + * @see ServerResponse.status + */ + fun status(status: Int) = ServerResponse.status(status) + +} + +/** + * Equivalent to [RouterFunction.and]. + */ +operator fun RouterFunction.plus(other: RouterFunction) = + this.and(other) \ No newline at end of file diff --git a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/ServerRequestExtensions.kt b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/ServerRequestExtensions.kt new file mode 100644 index 0000000000..2ac6ec7d16 --- /dev/null +++ b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/ServerRequestExtensions.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2019 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.web.servlet.function + +import org.springframework.core.ParameterizedTypeReference +import org.springframework.http.MediaType +import java.net.InetSocketAddress +import java.security.Principal + +/** + * Nullable variant of [ServerRequest.remoteAddress] + * + * @author Sebastien Deleuze + * @since 5.2 + */ +fun ServerRequest.remoteAddressOrNull(): InetSocketAddress? = remoteAddress().orElse(null) + +/** + * Extension for [ServerRequest.body] providing a `body()` variant + * leveraging Kotlin reified type parameters. This extension is not subject to type + * erasure and retains actual generic type arguments. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +inline fun ServerRequest.body(): T = body(object : ParameterizedTypeReference() {}) + + +/** + * Nullable variant of [ServerRequest.attribute] + * + * @author Sebastien Deleuze + * @since 5.2 + */ +fun ServerRequest.attributeOrNull(name: String): Any? = attribute(name).orElse(null) + +/** + * Nullable variant of [ServerRequest.param] + * + * @author Sebastien Deleuze + * @since 5.2 + */ +fun ServerRequest.paramOrNull(name: String): String? = param(name).orElse(null) + +/** + * Nullable variant of [ServerRequest.param] + * + * @author Sebastien Deleuze + * @since 5.2 + */ +fun ServerRequest.principalOrNull(): Principal? = principal().orElse(null) + +/** + * Nullable variant of [ServerRequest.Headers.contentLength] + * + * @author Sebastien Deleuze + * @since 5.2 + */ +fun ServerRequest.Headers.contentLengthOrNull(): Long? = contentLength().let { if (it.isPresent) it.asLong else null } + +/** + * Nullable variant of [ServerRequest.Headers.contentType] + * + * @author Sebastien Deleuze + * @since 5.2 + */ +fun ServerRequest.Headers.contentTypeOrNull(): MediaType? = contentType().orElse(null) + + + diff --git a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/ServerResponseExtensions.kt b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/ServerResponseExtensions.kt new file mode 100644 index 0000000000..fdf3e75292 --- /dev/null +++ b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/ServerResponseExtensions.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2019 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.web.servlet.function + +import org.springframework.core.ParameterizedTypeReference + +/** + * Extension for [ServerResponse.BodyBuilder.body] providing a variant + * leveraging Kotlin reified type parameters. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +inline fun ServerResponse.BodyBuilder.bodyWithType(body: T) = + body(body, object : ParameterizedTypeReference() {}) diff --git a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt new file mode 100644 index 0000000000..a927b67a7c --- /dev/null +++ b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2002-2019 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.web.servlet.function + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.springframework.core.io.ClassPathResource +import org.springframework.http.HttpHeaders.* +import org.springframework.http.HttpMethod.* +import org.springframework.http.MediaType.* +import org.springframework.mock.web.test.MockHttpServletRequest + +/** + * Tests for WebMvc.fn [RouterFunctionDsl]. + * + * @author Sebastien Deleuze + */ +class RouterFunctionDslTests { + + @Test + fun header() { + val servletRequest = MockHttpServletRequest() + servletRequest.addHeader("bar", "bar") + val request = DefaultServerRequest(servletRequest, emptyList()) + assertTrue(sampleRouter().route(request).isPresent) + } + + @Test + fun accept() { + val servletRequest = MockHttpServletRequest("GET", "/content") + servletRequest.addHeader(ACCEPT, APPLICATION_ATOM_XML_VALUE) + val request = DefaultServerRequest(servletRequest, emptyList()) + assertTrue(sampleRouter().route(request).isPresent) + } + + @Test + fun acceptAndPOST() { + val servletRequest = MockHttpServletRequest("POST", "/api/foo/") + servletRequest.addHeader(ACCEPT, APPLICATION_JSON_VALUE) + val request = DefaultServerRequest(servletRequest, emptyList()) + assertTrue(sampleRouter().route(request).isPresent) + } + + @Test + fun contentType() { + val servletRequest = MockHttpServletRequest("GET", "/content") + servletRequest.addHeader(CONTENT_TYPE, APPLICATION_OCTET_STREAM_VALUE) + val request = DefaultServerRequest(servletRequest, emptyList()) + assertTrue(sampleRouter().route(request).isPresent) + } + + @Test + fun resourceByPath() { + val servletRequest = MockHttpServletRequest("GET", "/org/springframework/web/servlet/function/response.txt") + val request = DefaultServerRequest(servletRequest, emptyList()) + assertTrue(sampleRouter().route(request).isPresent) + } + + @Test + fun method() { + val servletRequest = MockHttpServletRequest("PATCH", "/") + val request = DefaultServerRequest(servletRequest, emptyList()) + assertTrue(sampleRouter().route(request).isPresent) + } + + @Test + fun path() { + val servletRequest = MockHttpServletRequest("GET", "/baz") + val request = DefaultServerRequest(servletRequest, emptyList()) + assertTrue(sampleRouter().route(request).isPresent) + } + + @Test + fun resource() { + val servletRequest = MockHttpServletRequest("GET", "/response.txt") + val request = DefaultServerRequest(servletRequest, emptyList()) + assertTrue(sampleRouter().route(request).isPresent) + } + + @Test + fun noRoute() { + + val servletRequest = MockHttpServletRequest("GET", "/bar") + servletRequest.addHeader(ACCEPT, APPLICATION_PDF_VALUE) + servletRequest.addHeader(CONTENT_TYPE, APPLICATION_PDF_VALUE) + val request = DefaultServerRequest(servletRequest, emptyList()) + assertFalse(sampleRouter().route(request).isPresent) + } + + @Test + fun rendering() { + val servletRequest = MockHttpServletRequest("GET", "/rendering") + val request = DefaultServerRequest(servletRequest, emptyList()) + assertTrue(sampleRouter().route(request).get().handle(request) is RenderingResponse) + } + + @Test(expected = IllegalStateException::class) + fun emptyRouter() { + router { } + } + + + private fun sampleRouter() = router { + (GET("/foo/") or GET("/foos/")) { req -> handle(req) } + "/api".nest { + POST("/foo/", ::handleFromClass) + PUT("/foo/", :: handleFromClass) + PATCH("/foo/") { + ok().build() + } + "/foo/" { handleFromClass(it) } + } + "/content".nest { + accept(APPLICATION_ATOM_XML, ::handle) + contentType(APPLICATION_OCTET_STREAM, ::handle) + } + method(PATCH, ::handle) + headers { it.accept().contains(APPLICATION_JSON) }.nest { + GET("/api/foo/", ::handle) + } + headers({ it.header("bar").isNotEmpty() }, ::handle) + resources("/org/springframework/web/servlet/function/**", + ClassPathResource("/org/springframework/web/servlet/function/response.txt")) + resources { + if (it.path() == "/response.txt") { + ClassPathResource("/org/springframework/web/servlet/function/response.txt") + } + else { + null + } + } + path("/baz", ::handle) + GET("/rendering") { RenderingResponse.create("index").build() } + } +} + +@Suppress("UNUSED_PARAMETER") +private fun handleFromClass(req: ServerRequest) = ServerResponse.ok().build() + +@Suppress("UNUSED_PARAMETER") +private fun handle(req: ServerRequest) = ServerResponse.ok().build() diff --git a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/ServerRequestExtensionsTests.kt b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/ServerRequestExtensionsTests.kt new file mode 100644 index 0000000000..17704b4870 --- /dev/null +++ b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/ServerRequestExtensionsTests.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2002-2019 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.web.servlet.function + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.springframework.core.ParameterizedTypeReference +import org.springframework.http.MediaType +import java.net.InetSocketAddress +import java.security.Principal +import java.util.* + +/** + * Tests for WebMvc.fn [ServerRequest] extensions. + * + * @author Sebastien Deleuze + */ +class ServerRequestExtensionsTests { + + val request = mockk() + + val headers = mockk() + + @Test + fun `remoteAddressOrNull with value`() { + val remoteAddress = mockk() + every { request.remoteAddress() } returns Optional.of(remoteAddress) + assertEquals(remoteAddress, request.remoteAddressOrNull()) + verify { request.remoteAddress() } + } + + @Test + fun `remoteAddressOrNull with null`() { + every { request.remoteAddress() } returns Optional.empty() + assertNull(request.remoteAddressOrNull()) + verify { request.remoteAddress() } + } + + @Test + fun body() { + val body = Arrays.asList("foo", "bar") + val typeReference = object: ParameterizedTypeReference>() {} + every { request.body(typeReference) } returns body + assertEquals(body, request.body>()) + verify { request.body(typeReference) } + } + + @Test + fun `attributeOrNull with value`() { + val attribute = mockk() + every { request.attribute("foo") } returns Optional.of(attribute) + assertEquals(attribute, request.attributeOrNull("foo")) + verify { request.attribute("foo") } + } + + @Test + fun `attributeOrNull with null`() { + every { request.attribute("foo") } returns Optional.empty() + assertNull(request.attributeOrNull("foo")) + verify { request.attribute("foo") } + } + + @Test + fun `paramOrNull with value`() { + val param = "bar" + every { request.param("foo") } returns Optional.of(param) + assertEquals(param, request.paramOrNull("foo")) + verify { request.param("foo") } + } + + @Test + fun `paramOrNull with null`() { + every { request.param("foo") } returns Optional.empty() + assertNull(request.paramOrNull("foo")) + verify { request.param("foo") } + } + + @Test + fun `principalOrNull with value`() { + val principal = mockk() + every { request.principal() } returns Optional.of(principal) + assertEquals(principal, request.principalOrNull()) + verify { request.principal() } + } + + @Test + fun `principalOrNull with null`() { + every { request.principal() } returns Optional.empty() + assertNull(request.principalOrNull()) + verify { request.principal() } + } + + @Test + fun `contentLengthOrNull with value`() { + val contentLength: Long = 123 + every { headers.contentLength() } returns OptionalLong.of(contentLength) + assertEquals(contentLength, headers.contentLengthOrNull()) + verify { headers.contentLength() } + } + + @Test + fun `contentLengthOrNull with null`() { + every { headers.contentLength() } returns OptionalLong.empty() + assertNull(headers.contentLengthOrNull()) + verify { headers.contentLength() } + } + + @Test + fun `contentTypeOrNull with value`() { + val contentType = mockk() + every { headers.contentType() } returns Optional.of(contentType) + assertEquals(contentType, headers.contentTypeOrNull()) + verify { headers.contentType() } + } + + @Test + fun `contentTypeOrNull with null`() { + val contentType = mockk() + every { headers.contentType() } returns Optional.empty() + assertNull(headers.contentTypeOrNull()) + verify { headers.contentType() } + } +} diff --git a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/ServerResponseExtensionsTests.kt b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/ServerResponseExtensionsTests.kt new file mode 100644 index 0000000000..fe4bae0450 --- /dev/null +++ b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/ServerResponseExtensionsTests.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2019 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.web.servlet.function + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.Assert +import org.junit.Test +import org.springframework.core.ParameterizedTypeReference +import java.util.* + +/** + * Tests for WebMvc.fn [ServerResponse] extensions. + * + * @author Sebastien Deleuze + */ +class ServerResponseExtensionsTests { + + @Test + fun bodyWithType() { + val builder = mockk() + val response = mockk() + val body = Arrays.asList("foo", "bar") + val typeReference = object: ParameterizedTypeReference>() {} + every { builder.body(body, typeReference) } returns response + Assert.assertEquals(response, builder.bodyWithType>(body)) + verify { builder.body(body, typeReference) } + } +}