diff --git a/org.springframework.web/src/main/java/org/springframework/web/filter/HttpPutFormContentFilter.java b/org.springframework.web/src/main/java/org/springframework/web/filter/HttpPutFormContentFilter.java new file mode 100644 index 00000000000..a0b10b7dac0 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/filter/HttpPutFormContentFilter.java @@ -0,0 +1,151 @@ +/* + * Copyright 2002-2011 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 + * + * http://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.filter; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpInputMessage; +import org.springframework.http.converter.FormHttpMessageConverter; +import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * {@link javax.servlet.Filter} that makes form encoded data available through the + * {@code ServletRequest.getParameter*()} family of methods during HTTP PUT requests. + * + *

The Servlet spec requires form data to be available for HTTP POST but not for + * HTTP PUT requests. This filter intercepts HTTP PUT requests + * where {@code 'Content-Type:application/x-www-form-urlencoded'}, reads the form + * data from the body of the request, and wraps the ServletRequest in order to make + * the form data available as request parameters. + * + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class HttpPutFormContentFilter extends OncePerRequestFilter { + + private static final String FORM_CONTENT_TYPE = "application/x-www-form-urlencoded"; + + private final FormHttpMessageConverter formConverter = new XmlAwareFormHttpMessageConverter(); + + /** + * The default character set to use for reading form data. + */ + public void setCharset(Charset charset) { + this.formConverter.setCharset(charset); + } + + @Override + protected void doFilterInternal(final HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + if ("PUT".equals(request.getMethod()) && isFormContentType(request)) { + HttpInputMessage inputMessage = new ServletServerHttpRequest(request) { + @Override + public InputStream getBody() throws IOException { + return request.getInputStream(); + } + }; + MultiValueMap formParameters = formConverter.read(null, inputMessage); + HttpServletRequest wrapper = new HttpPutFormContentRequestWrapper(request, formParameters); + filterChain.doFilter(wrapper, response); + } + else { + filterChain.doFilter(request, response); + } + + } + + private boolean isFormContentType(HttpServletRequest request) { + String contentType = request.getContentType(); + return ((contentType != null) && contentType.equals(FORM_CONTENT_TYPE)); + } + + private static class HttpPutFormContentRequestWrapper extends HttpServletRequestWrapper { + + private MultiValueMap formParameters; + + public HttpPutFormContentRequestWrapper(HttpServletRequest request, MultiValueMap parameters) { + super(request); + this.formParameters = (parameters != null) ? parameters : new LinkedMultiValueMap(); + } + + @Override + public String getParameter(String name) { + String queryStringValue = super.getParameter(name); + String formValue = this.formParameters.getFirst(name); + return (queryStringValue != null) ? queryStringValue : formValue; + } + + @Override + public Map getParameterMap() { + Map result = new LinkedHashMap(); + Enumeration names = this.getParameterNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + result.put(name, this.getParameterValues(name)); + } + return result; + } + + @Override + public Enumeration getParameterNames() { + Set names = new LinkedHashSet(); + names.addAll(Collections.list(super.getParameterNames())); + names.addAll(this.formParameters.keySet()); + return Collections.enumeration(names); + } + + @Override + public String[] getParameterValues(String name) { + String[] queryStringValues = super.getParameterValues(name); + List formValues = this.formParameters.get(name); + if (formValues == null) { + return queryStringValues; + } + else if (queryStringValues == null) { + return formValues.toArray(new String[formValues.size()]); + } + else { + List result = new ArrayList(); + result.addAll(Arrays.asList(queryStringValues)); + result.addAll(formValues); + return result.toArray(new String[result.size()]); + } + } + } + +} diff --git a/org.springframework.web/src/test/java/org/springframework/mock/web/MockFilterChain.java b/org.springframework.web/src/test/java/org/springframework/mock/web/MockFilterChain.java new file mode 100644 index 00000000000..04281abde1d --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/mock/web/MockFilterChain.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2011 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 + * + * http://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.mock.web; + +import javax.servlet.FilterChain; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.springframework.util.Assert; + +/** + * Mock implementation of the {@link javax.servlet.FilterConfig} interface. + * + *

Used for testing the web framework; also useful for testing + * custom {@link javax.servlet.Filter} implementations. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see MockFilterConfig + * @see PassThroughFilterChain + */ +public class MockFilterChain implements FilterChain { + + private ServletRequest request; + + private ServletResponse response; + + + /** + * Records the request and response. + */ + public void doFilter(ServletRequest request, ServletResponse response) { + Assert.notNull(request, "Request must not be null"); + Assert.notNull(response, "Response must not be null"); + if (this.request != null) { + throw new IllegalStateException("This FilterChain has already been called!"); + } + this.request = request; + this.response = response; + } + + /** + * Return the request that {@link #doFilter} has been called with. + */ + public ServletRequest getRequest() { + return this.request; + } + + /** + * Return the response that {@link #doFilter} has been called with. + */ + public ServletResponse getResponse() { + return this.response; + } + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/filter/HttpPutFormContentFilterTests.java b/org.springframework.web/src/test/java/org/springframework/web/filter/HttpPutFormContentFilterTests.java new file mode 100644 index 00000000000..a8b58ad14a3 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/filter/HttpPutFormContentFilterTests.java @@ -0,0 +1,178 @@ +/* + * Copyright 2002-2011 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 + * + * http://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.filter; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +/** + * Test fixture for {@link HttpPutFormContentFilter}. + * + * @author Rossen Stoyanchev + */ +public class HttpPutFormContentFilterTests { + + private HttpPutFormContentFilter filter; + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + private MockFilterChain filterChain; + + @Before + public void setup() { + filter = new HttpPutFormContentFilter(); + request = new MockHttpServletRequest("PUT", "/"); + request.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=ISO-8859-1"); + request.setContentType("application/x-www-form-urlencoded"); + response = new MockHttpServletResponse(); + filterChain = new MockFilterChain(); + } + + @Test + public void wrapPutOnly() throws Exception { + request.setContent("".getBytes("ISO-8859-1")); + String[] methods = new String[] {"GET", "POST", "DELETE", "HEAD", "OPTIONS", "TRACE"}; + for (String method : methods) { + request.setMethod(method); + filterChain = new MockFilterChain(); + filter.doFilter(request, response, filterChain); + assertSame("Should not wrap for HTTP method " + method, request, filterChain.getRequest()); + } + } + + @Test + public void wrapFormEncodedOnly() throws Exception { + request.setContent("".getBytes("ISO-8859-1")); + String[] contentTypes = new String[] {"text/plain", "multipart/form-data"}; + for (String contentType : contentTypes) { + request.setContentType(contentType); + filterChain = new MockFilterChain(); + filter.doFilter(request, response, filterChain); + assertSame("Should not wrap for content type " + contentType, request, filterChain.getRequest()); + } + } + + @Test + public void getParameter() throws Exception { + request.setContent("name=value".getBytes("ISO-8859-1")); + filter.doFilter(request, response, filterChain); + + assertEquals("value", filterChain.getRequest().getParameter("name")); + } + + @Test + public void queryStringParam() throws Exception { + request.addParameter("name", "value1"); + request.setContent("name=value2".getBytes("ISO-8859-1")); + filter.doFilter(request, response, filterChain); + + assertEquals("Query string parameters should be listed ahead of form parameters", + "value1", filterChain.getRequest().getParameter("name")); + } + + @Test + public void nullParameter() throws Exception { + request.setContent("name=value".getBytes("ISO-8859-1")); + filter.doFilter(request, response, filterChain); + + assertNull(filterChain.getRequest().getParameter("noSuchParam")); + } + + @Test + public void getParameterNames() throws Exception { + request.addParameter("name1", "value1"); + request.addParameter("name2", "value2"); + request.setContent("name1=value1&name3=value3&name4=value4".getBytes("ISO-8859-1")); + filter.doFilter(request, response, filterChain); + + List names = Collections.list(filterChain.getRequest().getParameterNames()); + assertEquals(Arrays.asList("name1", "name2", "name3", "name4"), names); + } + + @Test + public void getParameterValues() throws Exception { + request.addParameter("name", "value1"); + request.addParameter("name", "value2"); + request.setContent("name=value3&name=value4".getBytes("ISO-8859-1")); + filter.doFilter(request, response, filterChain); + + String[] values = filterChain.getRequest().getParameterValues("name"); + assertArrayEquals(new String[]{"value1", "value2", "value3", "value4"}, values); + } + + @Test + public void getQueryStringParameterValuesOnly() throws Exception { + request.addParameter("name", "value1"); + request.addParameter("name", "value2"); + request.setContent("anotherName=anotherValue".getBytes("ISO-8859-1")); + filter.doFilter(request, response, filterChain); + + String[] values = filterChain.getRequest().getParameterValues("name"); + assertArrayEquals(new String[]{"value1", "value2"}, values); + } + + @Test + public void getFormParameterValuesOnly() throws Exception { + request.addParameter("name", "value1"); + request.addParameter("name", "value2"); + request.setContent("anotherName=anotherValue".getBytes("ISO-8859-1")); + filter.doFilter(request, response, filterChain); + + String[] values = filterChain.getRequest().getParameterValues("anotherName"); + assertArrayEquals(new String[]{"anotherValue"}, values); + } + + @Test + public void noParameterValuesOnly() throws Exception { + request.addParameter("name", "value1"); + request.addParameter("name", "value2"); + request.setContent("anotherName=anotherValue".getBytes("ISO-8859-1")); + filter.doFilter(request, response, filterChain); + + String[] values = filterChain.getRequest().getParameterValues("noSuchParameter"); + assertNull(values); + } + + @Test + public void getParameterMap() throws Exception { + request.addParameter("name", "value1"); + request.addParameter("name", "value2"); + request.setContent("name=value3&name4=value4".getBytes("ISO-8859-1")); + filter.doFilter(request, response, filterChain); + + Map parameters = filterChain.getRequest().getParameterMap(); + assertEquals(2, parameters.size()); + assertArrayEquals(new String[] {"value1", "value2", "value3"}, parameters.get("name")); + assertArrayEquals(new String[] {"value4"}, parameters.get("name4")); + } + +} diff --git a/spring-framework-reference/src/mvc.xml b/spring-framework-reference/src/mvc.xml index 348b5280484..cf80b6ceb49 100644 --- a/spring-framework-reference/src/mvc.xml +++ b/spring-framework-reference/src/mvc.xml @@ -1538,166 +1538,139 @@ public ResponseEntity<String> handle(HttpEntity<byte[]> requestEntit

- Using <interfacename>@ModelAttribute</interfacename> on a controller method + Using <interfacename>@ModelAttribute</interfacename> on a method - The @ModelAttribute annotation can be used on a - method or on a method argument. This section explains its usage on a method while the - next section explains its usage on a method argument. + The @ModelAttribute annotation can be used on + methods or on method arguments. This section explains its usage on methods while the + next section explains its usage on method arguments. - An @ModelAttribute on a method indicates a method - for adding model attributes. A few examples: + An @ModelAttribute on a method indicates the purpose + of that method is to add one or more model attributes. Such methods support the same + argument types as @RequestMapping methods but cannot + be mapped directly to requests. Instead @ModelAttribute + methods in a controller are invoked before @RequestMapping + methods, within the same controller. A couple of examples: - @ModelAttribute -public void populateModel(Model model) { - model.addAttribute("types", this.clinic.getPetTypes(); - // add more attributes ... -} + +// Add one attribute +// The return value of the method is added to the model under the name "account" +// You can customize the name via @ModelAttribute("myAccount") @ModelAttribute -public void addAccount(@RequestParam String number, Model model) { - Account account = accountManager.findAccount(number); - model.addAttribute(account); +public Account addAccount(@RequestParam String number) { + return accountManager.findAccount(number); } +// Add multiple attributes + @ModelAttribute -public Account addAccount(@RequestParam String number) { - return accountManager.findAccount(number); +public void populateModel(@RequestParam String number, Model model) { + model.addAttribute(accountManager.findAccount(number)); + // add more ... } - @ModelAttribute methods are not associated - with any request mappings. Simply they are invoked prior - to the invocation of every - @RequestMapping method in the same controller. - In terms of method arguments they support the same argument types as - @RequestMapping methods, hence allowing access to request - parameters, path variables, and so on -- see - for a complete listing. - - There are two common use cases for @ModelAttribute - methods. One, is to add commonly needed model attributes - see the first example - above where the model is populated with pet types to be shown in a drop-down. - The second use case is to pre-load a command object - see the second and third - examples above with the Account object (more on command objects in the next section). + @ModelAttribute methods are used to populate + the model with commonly needed attributes for example to fill a drop-down with + states or with pet types, or to retrieve a command object like Account in order + to use it to represent the data on an HTML form. The latter case is further + discussed in the next section. Note the two styles of @ModelAttribute methods. - In one style, the method accepts a Model and adds any number - of model attributes to it. In the second style, the method adds an attribute implicitly - by having it as its return value. That results in the object being added as an - attribute to the model. You can choose between the two styles depending on - whether you need to add one model attribute or multiple. + In the first, the method adds an attribute implicitly + by returning it. In the second, the method accepts a Model + and adds any number of model attributes to it. + You can choose between the two styles depending on your needs. A controller can have any number of @ModelAttribute methods. All such methods are invoked before @RequestMapping - methods within the same controller each contributing model attributes. + methods of the same controller. - What happens when a model attribute name is not explicitly provided? In such cases - a default name is assigned to the model attribute. - For example if the method returns an object of type Account, - the default name used is "account". However, you can also change it to something else - through the value of the @ModelAttribute annotation. - Or if adding attributes directly to the Model, use the - appropriate overloaded addAttribute(..) method providing - both the attribute name and the attribute value. - - Lastly the @ModelAttribute annotation can also - be used directly on an @RequestMapping method. In this - cause, the return value of the @RequestMapping method - will be added as a model attribute and the view name will be derived using - a convention -- see . + + What happens when a model attribute name is not explicitly specified? + In such cases a default name is assigned to the model attribute based on its type. + For example if the method returns an object of type Account, + the default name used is "account". You can change that through + the value of the @ModelAttribute annotation. + If adding attributes directly to the Model, use the + appropriate overloaded addAttribute(..) method - i.e. + with or without an attribute name. + + + The @ModelAttribute annotation can be used on + @RequestMapping methods as well. In that case + the return value of the @RequestMapping method + is interpreted as a model attribute rather than as a view name. + The view name is derived from view name conventions instead much like for methods + returning void -- see .
- Using <interfacename>@ModelAttribute</interfacename> on a controller method argument + Using <interfacename>@ModelAttribute</interfacename> on a method argument - As explained in the previous section an @ModelAttribute - annotation can be used on a method or on a method argument. When used on a method argument, - the @ModelAttribute annotation indicates a command object. A command - object is an object with properties (e.g. Account rather than - a simple type like int or String) that is used to - populate a form or reversely to be populated with the data from an HTML form. - As you will see command objects can significantly automate the process of - working with forms -- rather than dealing with individual form - parameters one at a time you'll be have them in a one command object. + As explained in the previous section @ModelAttribute + can be used on methods or on method arguments. This section explains its usage on + method arguments. - To be more precise annotating a method argument with - @ModelAttribute means 3 things: - - The command object should be located and/or instantiated. - It should be populated with request parameters from the current request. - The object should be added to the model. - - See the following example: + An @ModelAttribute on a method argument indicates + the argument should be retrieved from the model. If not present in the model, the argument + should be instantiated first and then added to the model. Once present in the model, the + argument's fields should be populated from all request parameters that have matching names. + This is known as data binding in Spring MVC, a very useful mechanism that saves you from + having to parse each form field individually. @RequestMapping(value="/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST) -public String processSubmit(@ModelAttribute("pet") Pet pet) { +public String processSubmit(@ModelAttribute Pet pet) { } - The @ModelAttribute annotation - designates "pet" as a command object. But where does the object come from? There are two options. - It may have been added to the model by an @ModelAttribute - method as explained in the previous section. Or otherwise if it's not already in the model, it - is instantiated through the default constructor. Typically if the object needs to be retrieved - from a database first, you can create an @ModelAttribute - method to do that. Or otherwise rely on the default constructor. If multiple methods in the - controller work on the same object, you can have it stored in the HTTP session between requests. - That is explained in . + Given the above example where can the Pet instance come from? + There are several options: - - Using a URI template variable to retrieve a command object - - There is one other more advanced way of instantiating a command object. - It is useful when the command object can be retrieved based on a URI template - variable. Here is an example: + + It may already be in the model due to use of + @SessionAttributes + -- see . + It may already be in the model due to an + @ModelAttribute method in the same + controller -- as explained in the previous section. + It may be retrieved based on a URI template variable + and type converter (explained in more detail below). + It may be instantiated using its default constructor. + + + An @ModelAttribute method is a common + way to to retrieve an attribute from the database, which may optionally be + stored between requests through the use of + @SessionAttributes. + In some cases it may be convenient to retrieve the attribute by using + an URI template variable and a type converter. Here is an example: - + @RequestMapping(value="/accounts/{account}", method = RequestMethod.PUT) public String save(@ModelAttribute("account") Account account) { } - The name of the model attribute "account" matches to the name of a URI - template variable. Assuming there is a registered - Converter<String, Account> - or PropertyEditor that can turn a String - account number into an Account instance, those will be used - to provision the command object and have it added to the model. - If such a Converter or PropertyEditor does not exist, however the default - constructor will still be used. - - -
-
- Data binding and validation with <interfacename>@ModelAttribute</interfacename> - - As mentioned in the previous section an @ModelAttribute - annotation means 3 things: - - The command object should be located and/or instantiated. - It should be populated with request parameters from the current request. - The object should be added to the model. - - In this section we'll focus on the second: data binding and validation. - - After the command object has been provisioned using one of the methods described in the - previous section, the next step is to apply data binding and validation. - Spring's WebDataBinder does that by matching request parameters -- - usually form data fields but can also be query string parameters -- to command object - properties, including properties of nested objects, and populates those properties - accordingly also applying type conversion as necessary. - For example an object of type Person with - properties firstName and age - will be populated assuming the presence of such named form fields. - As a result of data binding the command object is populated with the form data. - - There are various ways to customize the data binding process. - For example you may wish to specify required fields, allowed and disallowed - fields, or extend type conversion. - See and also . + In this example the name of the model attribute (i.e. "account") + matches the name of a URI template variable. If you register + Converter<String, Account> + (or PropertyEditor) that can turn the + String-based account into an Account + instance, then the above example will work without the need for an + @ModelAttribute method. + + The next step is data binding. The WebDataBinder + class matches request parameter names -- including query string parameters and + form fields -- to model attribute fields by name. Matching fields are populated + after type conversion (from String to the target field type) has been applied + where necessary. + Data binding and validation are covered in . + Customizing the data binding process for a controller level is covered in + . As a result of data binding there may be errors such as missing required fields or type conversion errors. To check for such errors @@ -1716,11 +1689,11 @@ public String processSubmit(@ModelAttribute("pet") Pet pet } - BindingResult allows you to check if errors were found in - which case it's common to return to the same form where the errors - can be rendered with the help of Spring's <errors> form tag. + With a BindingResult you can check if errors were found in + which case it's common to render the same form where the errors + can be shown with the help of Spring's <errors> form tag. - In addition to data binding you can apply validation using your own custom + In addition to data binding you can also invoke validation using your own custom validator passing the same BindingResult that was used to record data binding errors. That allows for data binding and validation errors to be accumulated in one place and subsequently reported back to the user: @@ -1752,13 +1725,13 @@ public String processSubmit(@Valid @ModelAttribute("pet") } See and - for details on how to configure and use Spring's Validation support. + for details on how to configure and use validation.
- Specifying attributes to store in a session with - <classname>@SessionAttributes</classname> + Using <classname>@SessionAttributes</classname> to store model attributes + in the HTTP session between requests The type-level @SessionAttributes annotation declares session attributes used by a specific handler. @@ -1786,7 +1759,49 @@ public class EditPetForm {
+ +
+ Working with <literal>application/x-www-form-urlencoded</literal> data + + The previous sections covered use of @ModelAttribute + to support form submission requests from browser clients. The same annotation is + recommended for use with requests from non-browser clients as well. However there is + one notable difference when it comes to working with HTTP PUT requests. + Browsers can submit form data via HTTP GET or HTTP POST. Non-browser + clients can also submit forms via HTTP PUT. This presents a challenge because the Servlet + specification requires the ServletRequest.getParameter*() family + of methods to support form field access only for HTTP POST, not for HTTP PUT. + + + To support HTTP PUT requests, the spring-web module provides + the filter HttpPutFormContentFilter, which can be configured + in web.xml: + + +<filter> + <filter-name>httpPutFormFilter</filter-name> + <filter-class>org.springframework.web.filter.HttpPutFormContentFilter</filter-class> +</filter> +<filter-mapping> + <filter-name>httpPutFormFilter</filter-name> + <servlet-name>dispatcherServlet</servlet-name> +</filter-mapping> + +<servlet> + <servlet-name>dispatcherServlet</servlet-name> + <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> +</servlet> + + + The above filter intercepts HTTP PUT requests with content type + application/x-www-form-urlencoded, reads the form data from the + body of the request, and wraps the ServletRequest in order + to make the form data available through the ServletRequest.getParameter*() + family of methods. + +
+
Mapping cookie values with the @CookieValue annotation