diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-httpexchange.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-httpexchange.adoc new file mode 100644 index 0000000000..f6d4fe7a41 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-httpexchange.adoc @@ -0,0 +1,215 @@ +[[webflux-ann-httpexchange]] += HttpExchange + +[.small]#xref:web/webmvc/mvc-controller/ann-httpexchange.adoc[See equivalent in the Servlet stack]# + +Similarly to +xref:web/webflux/controller/ann-requestmapping.adoc[`@RequestMapping`], +you can use the `@HttpExchange` annotation to map requests to controllers +methods. However, while `@RequestMapping` is only supported on the server side, `@HttpExchange` can be used both to create a server-side mapping and +xref:integration/rest-clients.adoc#rest-http-interface[an HTTP +Interface Client] that allows making requests. + +`@HttpExchange` has various attributes to match by URL, HTTP method, and media +types. You can use it at the class level to express shared mappings or at the +method level to narrow down to a specific endpoint mapping. + +There are also HTTP method specific shortcut variants of `@HttpExchange`: + +* `@GetExchange` +* `@PostExchange` +* `@PutExchange` +* `@DeleteExchange` +* `@PatchExchange` + +// TODO +The shortcuts are xref:web/webflux/controller/ann-httpexchange.adoc#webflux-ann-httpexchange-composed[Custom Annotations] that are provided +because, arguably, most controller methods should be mapped to a specific +HTTP method versus using `@HttpExchange`, which, by default, matches +to all HTTP methods. +An `@HttpExchange` is still needed at the class level to express shared mappings. + +The following example has type and method level mappings: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + @RestController + @HttpExchange("/persons") + class PersonController { + + @GetExchange("/{id}") + public Person getPerson(@PathVariable Long id) { + // ... + } + + @PostExchange + @ResponseStatus(HttpStatus.CREATED) + public void add(@RequestBody Person person) { + // ... + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + @RestController + @HttpExchange("/persons") + class PersonController { + + @GetExchange("/{id}") + fun getPerson(@PathVariable id: Long): Person { + // ... + } + + @PostExchange + @ResponseStatus(HttpStatus.CREATED) + fun add(@RequestBody person: Person) { + // ... + } + } +---- +====== + + +`@HttpExhange` supports a very similar method signature to `@MessageMapping`, +however, since it needs to be suitable both for requester and responder use, +there are slight differences. + +[[webflux-ann-httpexchange-uri-templates]] +== URI patterns +[.small]#xref:web/webmvc/mvc-controller/ann-httpexchange.adoc#mvc-ann-httpexchange-uri-templates[See equivalent in the Servlet stack]# + +URI patterns resolution support is very similar to the one offered by xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-uri-templates[`@RequestMapping`], with the difference +that while `@RequestMapping` accepts a `String` array as its `value` or `path` +parameter that is used to specify the URI patterns, only a single `String` can be passed +as the `value` of `@HttpExchange`. + +[[webflux-ann-httpexchange-contenttype]] +== Consumable Media Types +[.small]#xref:web/webmvc/mvc-controller/ann-httpexchange.adoc#mvc-ann-httpexchange-contenttype[See equivalent in the Servlet stack]# + +You can narrow the request mapping based on the `Content-Type` of the request, +as the following example shows: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + @PostExchange(path = "/pets", contentType = "application/json") // <1> + public void addPet(@RequestBody Pet pet) { + // ... + } +---- +<1> Using a `contentType` attribute to narrow the mapping by the content type. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + @PostExchange("/pets", contentType = "application/json") // <1> + fun addPet(@RequestBody pet: Pet) { + // ... + } +---- +<1> Using a `contentType` attribute to narrow the mapping by the content type. +====== + +The `contentType` attribute accepts a single `String` as the attribute value. + +You can also declare a shared `contentType` attribute at the class level. +Unlike most other request-mapping attributes, however, when used at the +class level, a method-level `contentType` attribute overrides rather than +extends the class-level declaration. + +TIP: `MediaType` provides constants for commonly used media types, such as +`APPLICATION_JSON_VALUE` and `APPLICATION_XML_VALUE`. + + +[[webflux-ann-httpexchange-accept]] +== Producible Media Types +[.small]#xref:web/webmvc/mvc-controller/ann-httpexchange.adoc#mvc-ann-httpexchange-accept[See equivalent in the Servlet stack]# + +You can narrow the request mapping based on the `Accept` request header and the list of +content types that a controller method produces, as the following example shows: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + @GetExchange(path = "/pets/{petId}", accept = "application/json") // <1> + @ResponseBody + public Pet getPet(@PathVariable String petId) { + // ... + } +---- +<1> Using an `accept` attribute to narrow the mapping by the content type that +can be served. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + @GetExchange("/pets/{petId}", accept = ["application/json"]) // <1> + @ResponseBody + fun getPet(@PathVariable petId: String): Pet { + // ... + } +---- +<1> Using an `accept` attribute to narrow the mapping by the content type that +can be served. +====== + +The `accept` attribute accepts a `String` array as the attribute value. + +You can declare a shared `accept` attribute at the class level. Unlike most +other request-mapping attributes, however, when used at the class level, +a method-level `accept` attribute +overrides rather than extends the class-level declaration. + +TIP: `MediaType` provides constants for commonly used media types, such as +`APPLICATION_JSON_VALUE` and `APPLICATION_XML_VALUE`. + + +[[webflux-ann-httpexchange-params-and-headers]] +== Parameters, headers +[.small]#xref:web/webmvc/mvc-controller/ann-httpexchange.adoc#mvc-ann-httpexchange-params-and-headers[See equivalent in the Servlet stack]# + +You can narrow request mappings based on request parameter and headers +conditions. It is supported for `@HttpExchange` in the same way as in xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-params-and-headers[`@RequestMapping` parameters and headers support]. + + +[[webflux-ann-httpexchange-head-options]] +== HTTP HEAD, OPTIONS +[.small]#xref:web/webmvc/mvc-controller/ann-httpexchange.adoc#mvc-ann-httpexchange-head-options[See equivalent in the Servlet stack]# + +The support of `HTTP HEAD` and `HTTP OPTIONS` in `@HttpExchange` annotated +controllers is the same xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-head-options[ +as in `@RequestMapping` annotated controllers]. + +[[webflux-ann-httpexchange-composed]] +== Custom Annotations +[.small]#xref:web/webmvc/mvc-controller/ann-httpexchange.adoc#mvc-ann-httpexchange-composed[See equivalent in the Servlet stack]# + +`@HttpExchange` annotated controllers support the use of xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[composed annotations] +for request mapping. Those are annotations that are themselves meta-annotated +with `@HttpExchange` and composed to redeclare a subset (or all) of the +`@HttpExchange` attributes with a narrower, more specific purpose. + +`@GetExchange`, `@PostExchange`, `@PutExchange`, `@DeleteExchange`, +and `@PatcExchange` are examples of composed annotations. They are provided +because, arguably, most controller methods should be mapped to a specific +HTTP method versus using `@HttpExchange`, which, by default, +matches to all HTTP methods. If you need an example of composed annotations, +look at how those are declared. + + diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods.adoc index 6930c9dbda..9a01e6f26f 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods.adoc @@ -4,7 +4,7 @@ [.small]#xref:web/webmvc/mvc-controller/ann-methods.adoc[See equivalent in the Servlet stack]# -`@RequestMapping` handler methods have a flexible signature and can choose from a range of +`@RequestMapping` and `@HttpExchange` handler methods have a flexible signature and can choose from a range of supported controller method arguments and return values. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-httpexchange.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-httpexchange.adoc new file mode 100644 index 0000000000..21ef7588bd --- /dev/null +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-httpexchange.adoc @@ -0,0 +1,215 @@ +[[mvc-ann-httpexchange]] += HttpExchange + +[.small]#xref:web/webflux/controller/ann-httpexchange.adoc[ +See equivalent in the Reactive stack]# + +Similarly to +xref:web/webmvc/mvc-controller/ann-requestmapping.adoc[`@RequestMapping`], +you can use the `@HttpExchange` annotation to map requests to controllers +methods. However, while `@RequestMapping` is only supported on the server side, `@HttpExchange` can be used both to create a server-side mapping and +xref:integration/rest-clients.adoc#rest-http-interface[an HTTP +Interface Client] that allows making requests. + +`@HttpExchange` has various attributes to match by URL, HTTP method, and media +types. You can use it at the class level to express shared mappings or at the +method level to narrow down to a specific endpoint mapping. + +There are also HTTP method specific shortcut variants of `@HttpExchange`: + +* `@GetExchange` +* `@PostExchange` +* `@PutExchange` +* `@DeleteExchange` +* `@PatchExchange` + +The shortcuts are xref:web/webmvc/mvc-controller/ann-httpexchange.adoc#mvc-ann-httpexchange-composed[Custom Annotations] that are provided +because, arguably, most controller methods should be mapped to a specific +HTTP method versus using `@HttpExchange`, which, by default, matches +to all HTTP methods. +An `@HttpExchange` is still needed at the class level to express shared mappings. + +The following example has type and method level mappings: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + @RestController + @HttpExchange("/persons") + class PersonController { + + @GetExchange("/{id}") + public Person getPerson(@PathVariable Long id) { + // ... + } + + @PostExchange + @ResponseStatus(HttpStatus.CREATED) + public void add(@RequestBody Person person) { + // ... + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + @RestController + @HttpExchange("/persons") + class PersonController { + + @GetExchange("/{id}") + fun getPerson(@PathVariable id: Long): Person { + // ... + } + + @PostExchange + @ResponseStatus(HttpStatus.CREATED) + fun add(@RequestBody person: Person) { + // ... + } + } +---- +====== + + +`@HttpExhange` supports a very similar method signature to `@MessageMapping`, +however, since it needs to be suitable both for requester and responder use, +there are slight differences, which are discussed below. + +[[mvc-ann-httpexchange-uri-templates]] +== URI patterns +[.small]#xref:web/webflux/controller/ann-httpexchange.adoc#webflux-ann-httpexchange-uri-templates[See equivalent in the Reactive stack]# + +URI patterns resolution support is very similar to the one offered by xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-uri-templates[`@RequestMapping`], with the difference +that while `@RequestMapping` accepts a `String` array as its `value` or `path` +parameter that is used to specify the URI patterns, only a single `String` can be passed +as the `value` of `@HttpExchange`. + +[[mvc-ann-httpexchange-contenttype]] +== Consumable Media Types +[.small]#xref:web/webflux/controller/ann-httpexchange.adoc#webflux-ann-httpexchange-contenttype[See equivalent in the Reactive stack]# + +You can narrow the request mapping based on the `Content-Type` of the request, +as the following example shows: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + @PostExchange(path = "/pets", contentType = "application/json") // <1> + public void addPet(@RequestBody Pet pet) { + // ... + } +---- +<1> Using a `contentType` attribute to narrow the mapping by the content type. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + @PostExchange("/pets", contentType = "application/json") // <1> + fun addPet(@RequestBody pet: Pet) { + // ... + } +---- +<1> Using a `contentType` attribute to narrow the mapping by the content type. +====== + +The `contentType` attribute accepts a single `String` as the attribute value. + +You can also declare a shared `contentType` attribute at the class level. +Unlike most other request-mapping attributes, however, when used at the +class level, a method-level `contentType` attribute overrides rather than +extends the class-level declaration. + +TIP: `MediaType` provides constants for commonly used media types, such as +`APPLICATION_JSON_VALUE` and `APPLICATION_XML_VALUE`. + + +[[mvc-ann-httpexchange-accept]] +== Producible Media Types +[.small]#xref:web/webflux/controller/ann-httpexchange.adoc#webflux-ann-httpexchange-accept[See equivalent in the Reactive stack]# + +You can narrow the request mapping based on the `Accept` request header and the list of +content types that a controller method produces, as the following example shows: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + @GetExchange(path = "/pets/{petId}", accept = "application/json") // <1> + @ResponseBody + public Pet getPet(@PathVariable String petId) { + // ... + } +---- +<1> Using an `accept` attribute to narrow the mapping by the content type that +can be served. + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + @GetExchange("/pets/{petId}", accept = ["application/json"]) // <1> + @ResponseBody + fun getPet(@PathVariable petId: String): Pet { + // ... + } +---- +<1> Using an `accept` attribute to narrow the mapping by the content type that +can be served. +====== + +The `accept` attribute accepts a `String` array as the attribute value. + +You can declare a shared `accept` attribute at the class level. Unlike most +other request-mapping attributes, however, when used at the class level, +a method-level `accept` attribute +overrides rather than extends the class-level declaration. + +TIP: `MediaType` provides constants for commonly used media types, such as +`APPLICATION_JSON_VALUE` and `APPLICATION_XML_VALUE`. + + +[[mvc-ann-httpexchange-params-and-headers]] +== Parameters, headers +[.small]#xref:web/webflux/controller/ann-httpexchange.adoc#webflux-ann-httpexchange-params-and-headers[See equivalent in the Reactive stack]# + +You can narrow request mappings based on request parameter and headers +conditions. It is supported for `@HttpExchange` in the same way as in xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-params-and-headers[`@RequestMapping` parameter and header support] . + + +[[mvc-ann-httpexchange-head-options]] +== HTTP HEAD, OPTIONS +[.small]#xref:web/webflux/controller/ann-httpexchange.adoc#webflux-ann-httpexchange-head-options[See equivalent in the Reactive stack]# + +The support of `HTTP HEAD` and `HTTP OPTIONS` in `@HttpExchange` annotated +controllers is the same xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-head-options[ +as in `@RequestMapping` annotated controllers]. + +[[mvc-ann-httpexchange-composed]] +== Custom Annotations +[.small]#xref:web/webflux/controller/ann-httpexchange.adoc#webflux-ann-httpexchange-head-options[See equivalent in the Reactive stack]# + +`@HttpExchange` annotated controllers support the use of xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[composed annotations] +for request mapping. Those are annotations that are themselves meta-annotated +with `@HttpExchange` and composed to redeclare a subset (or all) of the +`@HttpExchange` attributes with a narrower, more specific purpose. + +`@GetExchange`, `@PostExchange`, `@PutExchange`, `@DeleteExchange`, +and `@PatcExchange` are examples of composed annotations. They are provided +because, arguably, most controller methods should be mapped to a specific +HTTP method versus using `@HttpExchange`, which, by default, +matches to all HTTP methods. If you need an example of composed annotations, +look at how those are declared. + + diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods.adoc index 853e26607d..a44b36cc9c 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods.adoc @@ -4,7 +4,7 @@ [.small]#xref:web/webflux/controller/ann-methods.adoc[See equivalent in the Reactive stack]# -`@RequestMapping` handler methods have a flexible signature and can choose from a range of +`@RequestMapping` and `@HttpExchange` handler methods have a flexible signature and can choose from a range of supported controller method arguments and return values. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java index d05339ef8e..631141e4cb 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java @@ -46,6 +46,7 @@ import org.springframework.web.reactive.result.condition.ConsumesRequestConditio import org.springframework.web.reactive.result.condition.RequestCondition; import org.springframework.web.reactive.result.method.RequestMappingInfo; import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping; +import org.springframework.web.service.annotation.HttpExchange; /** * An extension of {@link RequestMappingInfoHandlerMapping} that creates @@ -54,6 +55,7 @@ import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerM * * @author Rossen Stoyanchev * @author Sam Brannen + * @author Olga Maciaszek-Sharma * @since 5.0 */ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping @@ -171,18 +173,27 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi } /** - * Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)}, - * supplying the appropriate custom {@link RequestCondition} depending on whether + * Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)} + * or {@link #createRequestMappingInfo(HttpExchange, RequestCondition)}, + * depending on which annotation is found on the element being processed, + * and supplying the appropriate custom {@link RequestCondition} depending on whether * the supplied {@code annotatedElement} is a class or method. * @see #getCustomTypeCondition(Class) * @see #getCustomMethodCondition(Method) - */ + */ @Nullable private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) { - RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class); RequestCondition condition = (element instanceof Class clazz ? getCustomTypeCondition(clazz) : getCustomMethodCondition((Method) element)); - return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null); + RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class); + if(requestMapping != null){ + return createRequestMappingInfo(requestMapping, condition); + } + HttpExchange httpExchange = AnnotatedElementUtils.findMergedAnnotation(element, HttpExchange.class); + if(httpExchange != null){ + return createRequestMappingInfo(httpExchange, condition); + } + return null; } /** @@ -246,6 +257,28 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi return builder.options(this.config).build(); } + /** + * Create a {@link RequestMappingInfo} from the supplied + * {@link HttpExchange @HttpExchange} annotation, which is either + * a directly declared annotation, a meta-annotation, or the synthesized + * result of merging annotation attributes within an annotation hierarchy. + */ + protected RequestMappingInfo createRequestMappingInfo( + HttpExchange httpExchange, + @Nullable RequestCondition customCondition) { + + RequestMappingInfo.Builder builder = RequestMappingInfo + .paths(resolveEmbeddedValuesInPatterns( + toTextArray(httpExchange.value()))) + .methods(toMethodArray(httpExchange.method())) + .consumes(toTextArray(httpExchange.contentType())) + .produces(httpExchange.accept()); + if (customCondition != null) { + builder.customCondition(customCondition); + } + return builder.options(this.config).build(); + } + /** * Resolve placeholder values in the given array of patterns. * @return a new array with updated patterns @@ -263,6 +296,24 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi } } + + private static String[] toTextArray(String string) { + if (StringUtils.hasText(string)) { + return new String[] { string }; + } + return new String[] {}; + } + + private static RequestMethod[] toMethodArray(String method) { + RequestMethod requestMethod = null; + if (StringUtils.hasText(method)) { + requestMethod = RequestMethod.resolve(method); + } + return requestMethod != null ? new RequestMethod[] { requestMethod } + : new RequestMethod[] {}; + } + + @Override public void registerMapping(RequestMappingInfo mapping, Object handler, Method method) { super.registerMapping(mapping, handler, method); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java index fa53bc0824..7b2f285b18 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java @@ -44,8 +44,10 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.method.HandlerTypePredicate; import org.springframework.web.reactive.result.condition.ConsumesRequestCondition; +import org.springframework.web.reactive.result.condition.MediaTypeExpression; import org.springframework.web.reactive.result.condition.PatternsRequestCondition; import org.springframework.web.reactive.result.method.RequestMappingInfo; +import org.springframework.web.service.annotation.HttpExchange; import org.springframework.web.util.pattern.PathPattern; import org.springframework.web.util.pattern.PathPatternParser; @@ -56,6 +58,7 @@ import static org.mockito.Mockito.mock; * Unit tests for {@link RequestMappingHandlerMapping}. * * @author Rossen Stoyanchev + * @author Olga Maciaszek-Sharma */ class RequestMappingHandlerMappingTests { @@ -150,6 +153,52 @@ class RequestMappingHandlerMappingTests { assertComposedAnnotationMapping(RequestMethod.PATCH); } + @SuppressWarnings("DataFlowIssue") + @Test + void httpExchangeWithDefaultValues() throws NoSuchMethodException { + this.handlerMapping.afterPropertiesSet(); + + RequestMappingInfo mappingInfo = this.handlerMapping.getMappingForMethod( + HttpExchangeController.class.getMethod("defaultValuesExchange"), + HttpExchangeController.class); + + assertThat(mappingInfo.getPatternsCondition().getPatterns()) + .extracting(PathPattern::toString) + .containsOnly("/exchange"); + assertThat(mappingInfo.getMethodsCondition().getMethods()).isEmpty(); + assertThat(mappingInfo.getParamsCondition().getExpressions()).isEmpty(); + assertThat(mappingInfo.getHeadersCondition().getExpressions()).isEmpty(); + assertThat(mappingInfo.getConsumesCondition().getExpressions()).isEmpty(); + assertThat(mappingInfo.getProducesCondition().getExpressions()).isEmpty(); + } + + @SuppressWarnings("DataFlowIssue") + @Test + void httpExchangeWithCustomValues() throws NoSuchMethodException { + this.handlerMapping.afterPropertiesSet(); + + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + mapping.setApplicationContext(new StaticWebApplicationContext()); + mapping.afterPropertiesSet(); + + RequestMappingInfo mappingInfo = mapping.getMappingForMethod( + HttpExchangeController.class.getMethod("customValuesExchange"), + HttpExchangeController.class); + + assertThat(mappingInfo.getPatternsCondition().getPatterns()) + .extracting(PathPattern::toString) + .containsOnly("/exchange/custom"); + assertThat(mappingInfo.getMethodsCondition().getMethods()) + .containsOnly(RequestMethod.POST); + assertThat(mappingInfo.getParamsCondition().getExpressions()).isEmpty(); + assertThat(mappingInfo.getHeadersCondition().getExpressions()).isEmpty(); + assertThat(mappingInfo.getConsumesCondition().getExpressions()) + .extracting(MediaTypeExpression::getMediaType) + .containsOnly(MediaType.APPLICATION_JSON); + assertThat(mappingInfo.getProducesCondition().getExpressions()) + .extracting(MediaTypeExpression::getMediaType) + .containsOnly(MediaType.valueOf("text/plain;charset=UTF-8")); + } private RequestMappingInfo assertComposedAnnotationMapping(RequestMethod requestMethod) throws Exception { String methodName = requestMethod.name().toLowerCase(); @@ -238,4 +287,16 @@ class RequestMappingHandlerMappingTests { } } + @RestController + @HttpExchange("/exchange") + static class HttpExchangeController { + + @HttpExchange + public void defaultValuesExchange(){} + + @HttpExchange(value = "/custom", accept = "text/plain;charset=UTF-8", + method = "POST", contentType = "application/json") + public void customValuesExchange(){} + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java index d30fcc6a3a..73a29d0059 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java @@ -35,6 +35,7 @@ import org.springframework.lang.Nullable; import org.springframework.stereotype.Controller; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.annotation.CrossOrigin; @@ -43,6 +44,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.method.HandlerMethod; +import org.springframework.web.service.annotation.HttpExchange; import org.springframework.web.servlet.handler.MatchableHandlerMapping; import org.springframework.web.servlet.handler.RequestMatchResult; import org.springframework.web.servlet.mvc.condition.AbstractRequestCondition; @@ -71,6 +73,7 @@ import org.springframework.web.util.pattern.PathPatternParser; * @author Arjen Poutsma * @author Rossen Stoyanchev * @author Sam Brannen + * @author Olga Maciaszek-Sharma * @since 3.1 */ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping @@ -331,18 +334,27 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi } /** - * Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)}, - * supplying the appropriate custom {@link RequestCondition} depending on whether + * Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)} + * or {@link #createRequestMappingInfo(HttpExchange, RequestCondition)}, + * depending on which annotation is found on the element being processed, + * and supplying the appropriate custom {@link RequestCondition} depending on whether * the supplied {@code annotatedElement} is a class or method. * @see #getCustomTypeCondition(Class) * @see #getCustomMethodCondition(Method) */ @Nullable private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) { - RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class); RequestCondition condition = (element instanceof Class clazz ? getCustomTypeCondition(clazz) : getCustomMethodCondition((Method) element)); - return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null); + RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class); + if(requestMapping != null){ + return createRequestMappingInfo(requestMapping, condition); + } + HttpExchange httpExchange = AnnotatedElementUtils.findMergedAnnotation(element, HttpExchange.class); + if(httpExchange != null){ + return createRequestMappingInfo(httpExchange, condition); + } + return null; } /** @@ -400,6 +412,28 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi return builder.options(this.config).build(); } + /** + * Create a {@link RequestMappingInfo} from the supplied + * {@link HttpExchange @HttpExchange} annotation, which is either + * a directly declared annotation, a meta-annotation, or the synthesized + * result of merging annotation attributes within an annotation hierarchy. + */ + protected RequestMappingInfo createRequestMappingInfo( + HttpExchange httpExchange, + @Nullable RequestCondition customCondition) { + + RequestMappingInfo.Builder builder = RequestMappingInfo + .paths(resolveEmbeddedValuesInPatterns( + toTextArray(httpExchange.value()))) + .methods(toMethodArray(httpExchange.method())) + .consumes(toTextArray(httpExchange.contentType())) + .produces(httpExchange.accept()); + if (customCondition != null) { + builder.customCondition(customCondition); + } + return builder.options(this.config).build(); + } + /** * Resolve placeholder values in the given array of patterns. * @return a new array with updated patterns @@ -417,6 +451,24 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi } } + + private static String[] toTextArray(String string) { + if (StringUtils.hasText(string)) { + return new String[] { string }; + } + return new String[] {}; + } + + private static RequestMethod[] toMethodArray(String method) { + RequestMethod requestMethod = null; + if (StringUtils.hasText(method)) { + requestMethod = RequestMethod.resolve(method); + } + return requestMethod != null ? new RequestMethod[] { requestMethod } + : new RequestMethod[] {}; + } + + @Override public void registerMapping(RequestMappingInfo mapping, Object handler, Method method) { super.registerMapping(mapping, handler, method); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java index eab4ef9d4d..2cd69987be 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java @@ -47,11 +47,14 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.method.HandlerTypePredicate; +import org.springframework.web.service.annotation.HttpExchange; import org.springframework.web.servlet.handler.PathPatternsParameterizedTest; import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition; +import org.springframework.web.servlet.mvc.condition.MediaTypeExpression; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.util.ServletRequestPathUtils; +import org.springframework.web.util.pattern.PathPattern; import org.springframework.web.util.pattern.PathPatternParser; import static org.assertj.core.api.Assertions.assertThat; @@ -62,6 +65,7 @@ import static org.mockito.Mockito.mock; * * @author Rossen Stoyanchev * @author Sam Brannen + * @author Olga Maciaszek-Sharma */ public class RequestMappingHandlerMappingTests { @@ -278,6 +282,53 @@ public class RequestMappingHandlerMappingTests { assertComposedAnnotationMapping(RequestMethod.PATCH); } + @SuppressWarnings("DataFlowIssue") + @Test + void httpExchangeWithDefaultValues() throws NoSuchMethodException { + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + mapping.setApplicationContext(new StaticWebApplicationContext()); + mapping.afterPropertiesSet(); + + RequestMappingInfo mappingInfo = mapping.getMappingForMethod( + HttpExchangeController.class.getMethod("defaultValuesExchange"), + HttpExchangeController.class); + + assertThat(mappingInfo.getPathPatternsCondition().getPatterns()) + .extracting(PathPattern::toString) + .containsOnly("/exchange"); + assertThat(mappingInfo.getMethodsCondition().getMethods()).isEmpty(); + assertThat(mappingInfo.getParamsCondition().getExpressions()).isEmpty(); + assertThat(mappingInfo.getHeadersCondition().getExpressions()).isEmpty(); + assertThat(mappingInfo.getConsumesCondition().getExpressions()).isEmpty(); + assertThat(mappingInfo.getProducesCondition().getExpressions()).isEmpty(); + } + + @SuppressWarnings("DataFlowIssue") + @Test + void httpExchangeWithCustomValues() throws NoSuchMethodException { + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + mapping.setApplicationContext(new StaticWebApplicationContext()); + mapping.afterPropertiesSet(); + + RequestMappingInfo mappingInfo = mapping.getMappingForMethod( + HttpExchangeController.class.getMethod("customValuesExchange"), + HttpExchangeController.class); + + assertThat(mappingInfo.getPathPatternsCondition().getPatterns()) + .extracting(PathPattern::toString) + .containsOnly("/exchange/custom"); + assertThat(mappingInfo.getMethodsCondition().getMethods()) + .containsOnly(RequestMethod.POST); + assertThat(mappingInfo.getParamsCondition().getExpressions()).isEmpty(); + assertThat(mappingInfo.getHeadersCondition().getExpressions()).isEmpty(); + assertThat(mappingInfo.getConsumesCondition().getExpressions()) + .extracting(MediaTypeExpression::getMediaType) + .containsOnly(MediaType.APPLICATION_JSON); + assertThat(mappingInfo.getProducesCondition().getExpressions()) + .extracting(MediaTypeExpression::getMediaType) + .containsOnly(MediaType.valueOf("text/plain;charset=UTF-8")); + } + private RequestMappingInfo assertComposedAnnotationMapping(RequestMethod requestMethod) throws Exception { RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); @@ -367,6 +418,18 @@ public class RequestMappingHandlerMappingTests { } } + @RestController + @HttpExchange("/exchange") + static class HttpExchangeController { + + @HttpExchange + public void defaultValuesExchange(){} + + @HttpExchange(value = "/custom", accept = "text/plain;charset=UTF-8", + method = "POST", contentType = "application/json") + public void customValuesExchange(){} + } + private static class Foo { }