SPR-5628 Add HttpPutFormContentFilter in order to make form encoded data available via ServletRequest.getParameter*()
This commit is contained in:
parent
c31b17fef2
commit
2cf2fc195e
|
@ -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.
|
||||
*
|
||||
* <p>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<String, String> 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<String, String> formParameters;
|
||||
|
||||
public HttpPutFormContentRequestWrapper(HttpServletRequest request, MultiValueMap<String, String> parameters) {
|
||||
super(request);
|
||||
this.formParameters = (parameters != null) ? parameters : new LinkedMultiValueMap<String, String>();
|
||||
}
|
||||
|
||||
@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<String, String[]> getParameterMap() {
|
||||
Map<String, String[]> result = new LinkedHashMap<String, String[]>();
|
||||
Enumeration<String> names = this.getParameterNames();
|
||||
while (names.hasMoreElements()) {
|
||||
String name = names.nextElement();
|
||||
result.put(name, this.getParameterValues(name));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<String> getParameterNames() {
|
||||
Set<String> names = new LinkedHashSet<String>();
|
||||
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<String> formValues = this.formParameters.get(name);
|
||||
if (formValues == null) {
|
||||
return queryStringValues;
|
||||
}
|
||||
else if (queryStringValues == null) {
|
||||
return formValues.toArray(new String[formValues.size()]);
|
||||
}
|
||||
else {
|
||||
List<String> result = new ArrayList<String>();
|
||||
result.addAll(Arrays.asList(queryStringValues));
|
||||
result.addAll(formValues);
|
||||
return result.toArray(new String[result.size()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
* <p>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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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<String> 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<String, String[]> parameters = filterChain.getRequest().getParameterMap();
|
||||
assertEquals(2, parameters.size());
|
||||
assertArrayEquals(new String[] {"value1", "value2", "value3"}, parameters.get("name"));
|
||||
assertArrayEquals(new String[] {"value4"}, parameters.get("name4"));
|
||||
}
|
||||
|
||||
}
|
|
@ -1538,166 +1538,139 @@ public ResponseEntity<String> handle(HttpEntity<byte[]> requestEntit
|
|||
</section>
|
||||
|
||||
<section id="mvc-ann-modelattrib-methods">
|
||||
<title>Using <interfacename>@ModelAttribute</interfacename> on a controller method</title>
|
||||
<title>Using <interfacename>@ModelAttribute</interfacename> on a method</title>
|
||||
|
||||
<para>The <interfacename>@ModelAttribute</interfacename> 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.</para>
|
||||
<para>The <interfacename>@ModelAttribute</interfacename> 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.</para>
|
||||
|
||||
<para>An <interfacename>@ModelAttribute</interfacename> on a method indicates a method
|
||||
for adding model attributes. A few examples:</para>
|
||||
<para>An <interfacename>@ModelAttribute</interfacename> 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 <interfacename>@RequestMapping</interfacename> methods but cannot
|
||||
be mapped directly to requests. Instead <interfacename>@ModelAttribute</interfacename>
|
||||
methods in a controller are invoked before <interfacename>@RequestMapping</interfacename>
|
||||
methods, within the same controller. A couple of examples:</para>
|
||||
|
||||
<programlisting language="java">@ModelAttribute
|
||||
public void populateModel(Model model) {
|
||||
model.addAttribute("types", this.clinic.getPetTypes();
|
||||
// add more attributes ...
|
||||
}
|
||||
|
||||
@ModelAttribute
|
||||
public void addAccount(@RequestParam String number, Model model) {
|
||||
Account account = accountManager.findAccount(number);
|
||||
model.addAttribute(account);
|
||||
}
|
||||
<programlisting language="java">
|
||||
// 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 Account addAccount(@RequestParam String number) {
|
||||
return accountManager.findAccount(number);
|
||||
}
|
||||
|
||||
// Add multiple attributes
|
||||
|
||||
@ModelAttribute
|
||||
public void populateModel(@RequestParam String number, Model model) {
|
||||
model.addAttribute(accountManager.findAccount(number));
|
||||
// add more ...
|
||||
}
|
||||
</programlisting>
|
||||
|
||||
<para><interfacename>@ModelAttribute</interfacename> methods are not associated
|
||||
with any request mappings. Simply they are invoked prior
|
||||
to the invocation of every
|
||||
<interfacename>@RequestMapping</interfacename> method in the same controller.
|
||||
In terms of method arguments they support the same argument types as
|
||||
<interfacename>@RequestMapping</interfacename> methods, hence allowing access to request
|
||||
parameters, path variables, and so on -- see <xref linkend="mvc-ann-arguments"/>
|
||||
for a complete listing.</para>
|
||||
|
||||
<para>There are two common use cases for <interfacename>@ModelAttribute</interfacename>
|
||||
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).</para>
|
||||
<para><interfacename>@ModelAttribute</interfacename> 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.</para>
|
||||
|
||||
<para>Note the two styles of <interfacename>@ModelAttribute</interfacename> methods.
|
||||
In one style, the method accepts a <classname>Model</classname> 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.</para>
|
||||
In the first, the method adds an attribute implicitly
|
||||
by returning it. In the second, the method accepts a <classname>Model</classname>
|
||||
and adds any number of model attributes to it.
|
||||
You can choose between the two styles depending on your needs.</para>
|
||||
|
||||
<para>A controller can have any number of <interfacename>@ModelAttribute</interfacename>
|
||||
methods. All such methods are invoked before <interfacename>@RequestMapping</interfacename>
|
||||
methods within the same controller each contributing model attributes.</para>
|
||||
methods of the same controller.</para>
|
||||
|
||||
<para>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 <classname>Account</classname>,
|
||||
the default name used is "account". However, you can also change it to something else
|
||||
through the value of the <interfacename>@ModelAttribute</interfacename> annotation.
|
||||
Or if adding attributes directly to the <classname>Model</classname>, use the
|
||||
appropriate overloaded <literal>addAttribute(..)</literal> method providing
|
||||
both the attribute name and the attribute value.</para>
|
||||
<tip>
|
||||
<para>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 <classname>Account</classname>,
|
||||
the default name used is "account". You can change that through
|
||||
the value of the <interfacename>@ModelAttribute</interfacename> annotation.
|
||||
If adding attributes directly to the <classname>Model</classname>, use the
|
||||
appropriate overloaded <literal>addAttribute(..)</literal> method - i.e.
|
||||
with or without an attribute name.</para>
|
||||
</tip>
|
||||
|
||||
<para>Lastly the <interfacename>@ModelAttribute</interfacename> annotation can also
|
||||
be used directly on an <interfacename>@RequestMapping</interfacename> method. In this
|
||||
cause, the return value of the <interfacename>@RequestMapping</interfacename> method
|
||||
will be added as a model attribute and the view name will be derived using
|
||||
a convention -- see <xref linkend="mvc-coc-r2vnt"/>.</para>
|
||||
<para>The <interfacename>@ModelAttribute</interfacename> annotation can be used on
|
||||
<interfacename>@RequestMapping</interfacename> methods as well. In that case
|
||||
the return value of the <interfacename>@RequestMapping</interfacename> 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 <xref linkend="mvc-coc-r2vnt"/>.</para>
|
||||
|
||||
</section>
|
||||
|
||||
<section id="mvc-ann-modelattrib-method-args">
|
||||
<title>Using <interfacename>@ModelAttribute</interfacename> on a controller method argument</title>
|
||||
<title>Using <interfacename>@ModelAttribute</interfacename> on a method argument</title>
|
||||
|
||||
<para>As explained in the previous section an <interfacename>@ModelAttribute</interfacename>
|
||||
annotation can be used on a method or on a method argument. When used on a method argument,
|
||||
the <interfacename>@ModelAttribute</interfacename> annotation indicates a command object. A command
|
||||
object is an object with properties (e.g. <classname>Account</classname> rather than
|
||||
a simple type like <literal>int</literal> or <literal>String</literal>) 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.</para>
|
||||
<para>As explained in the previous section <interfacename>@ModelAttribute</interfacename>
|
||||
can be used on methods or on method arguments. This section explains its usage on
|
||||
method arguments.</para>
|
||||
|
||||
<para>To be more precise annotating a method argument with
|
||||
<interfacename>@ModelAttribute</interfacename> means 3 things:
|
||||
<orderedlist>
|
||||
<listitem>The command object should be located and/or instantiated.</listitem>
|
||||
<listitem>It should be populated with request parameters from the current request.</listitem>
|
||||
<listitem>The object should be added to the model.</listitem>
|
||||
</orderedlist>
|
||||
See the following example:
|
||||
<para>An <interfacename>@ModelAttribute</interfacename> 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.
|
||||
</para>
|
||||
|
||||
<programlisting language="java">
|
||||
@RequestMapping(value="/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST)
|
||||
public String processSubmit(<emphasis role="bold">@ModelAttribute("pet") Pet pet</emphasis>) {
|
||||
public String processSubmit(<emphasis role="bold">@ModelAttribute Pet pet</emphasis>) {
|
||||
|
||||
}</programlisting>
|
||||
|
||||
<para>The <interfacename>@ModelAttribute</interfacename> 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 <interfacename>@ModelAttribute</interfacename>
|
||||
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 <interfacename>@ModelAttribute</interfacename>
|
||||
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 <xref linkend="mvc-ann-sessionattrib"/>.</para>
|
||||
<para>Given the above example where can the Pet instance come from?
|
||||
There are several options: </para>
|
||||
|
||||
<note>
|
||||
<title>Using a URI template variable to retrieve a command object</title>
|
||||
|
||||
<para>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:</para>
|
||||
<itemizedlist>
|
||||
<listitem>It may already be in the model due to use of
|
||||
<interfacename>@SessionAttributes</interfacename>
|
||||
-- see <xref linkend="mvc-ann-sessionattrib"/>.</listitem>
|
||||
<listitem>It may already be in the model due to an
|
||||
<interfacename>@ModelAttribute</interfacename> method in the same
|
||||
controller -- as explained in the previous section.</listitem>
|
||||
<listitem>It may be retrieved based on a URI template variable
|
||||
and type converter (explained in more detail below).</listitem>
|
||||
<listitem>It may be instantiated using its default constructor.</listitem>
|
||||
</itemizedlist>
|
||||
|
||||
<para>An <interfacename>@ModelAttribute</interfacename> method is a common
|
||||
way to to retrieve an attribute from the database, which may optionally be
|
||||
stored between requests through the use of
|
||||
<interfacename>@SessionAttributes</interfacename>.
|
||||
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:</para>
|
||||
|
||||
<programlisting language="java">
|
||||
<programlisting language="java">
|
||||
@RequestMapping(value="/accounts/{account}", method = RequestMethod.PUT)
|
||||
public String save(@ModelAttribute("account") Account account) {
|
||||
|
||||
}</programlisting>
|
||||
<para>The name of the model attribute "account" matches to the name of a URI
|
||||
template variable. Assuming there is a registered
|
||||
<classname>Converter<String, Account></classname>
|
||||
or <classname>PropertyEditor</classname> that can turn a <literal>String</literal>
|
||||
account number into an <classname>Account</classname> 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.</para>
|
||||
</note>
|
||||
|
||||
<para>In this example the name of the model attribute (i.e. "account")
|
||||
matches the name of a URI template variable. If you register
|
||||
<classname>Converter<String, Account></classname>
|
||||
(or <classname>PropertyEditor</classname>) that can turn the
|
||||
<literal>String</literal>-based account into an <classname>Account</classname>
|
||||
instance, then the above example will work without the need for an
|
||||
<interfacename>@ModelAttribute</interfacename> method.</para>
|
||||
|
||||
</section>
|
||||
|
||||
<section id="mvc-ann-modelattrib-data-binding">
|
||||
<title>Data binding and validation with <interfacename>@ModelAttribute</interfacename></title>
|
||||
|
||||
<para>As mentioned in the previous section an <interfacename>@ModelAttribute</interfacename>
|
||||
annotation means 3 things:
|
||||
<orderedlist>
|
||||
<listitem>The command object should be located and/or instantiated.</listitem>
|
||||
<listitem>It should be populated with request parameters from the current request.</listitem>
|
||||
<listitem>The object should be added to the model.</listitem>
|
||||
</orderedlist>
|
||||
In this section we'll focus on the second: data binding and validation.</para>
|
||||
|
||||
<para>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 <classname>WebDataBinder</classname> 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 <classname>Person</classname> with
|
||||
properties <literal>firstName</literal> and <literal>age</literal>
|
||||
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.</para>
|
||||
|
||||
<para>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 <xref linkend="mvc-ann-webdatabinder"/> and also <xref linkend="validation"/>.</para>
|
||||
<para>The next step is data binding. The <classname>WebDataBinder</classname>
|
||||
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 <xref linkend="validation"/>.
|
||||
Customizing the data binding process for a controller level is covered in
|
||||
<xref linkend="mvc-ann-webdatabinder"/>.</para>
|
||||
|
||||
<para>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(<emphasis role="bold">@ModelAttribute("pet") Pet pet
|
|||
|
||||
}</programlisting>
|
||||
|
||||
<para><classname>BindingResult</classname> 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 <literal><errors></literal> form tag.</para>
|
||||
<para>With a <classname>BindingResult</classname> 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 <literal><errors></literal> form tag.</para>
|
||||
|
||||
<para>In addition to data binding you can apply validation using your own custom
|
||||
<para>In addition to data binding you can also invoke validation using your own custom
|
||||
validator passing the same <classname>BindingResult</classname> 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: </para>
|
||||
|
@ -1752,13 +1725,13 @@ public String processSubmit(<emphasis role="bold">@Valid @ModelAttribute("pet")
|
|||
}</programlisting>
|
||||
|
||||
<para>See <xref linkend="validation-beanvalidation"/> and
|
||||
<xref linkend="validation" /> for details on how to configure and use Spring's Validation support.</para>
|
||||
<xref linkend="validation" /> for details on how to configure and use validation.</para>
|
||||
|
||||
</section>
|
||||
|
||||
<section id="mvc-ann-sessionattrib">
|
||||
<title>Specifying attributes to store in a session with
|
||||
<classname>@SessionAttributes</classname></title>
|
||||
<title>Using <classname>@SessionAttributes</classname> to store model attributes
|
||||
in the HTTP session between requests</title>
|
||||
|
||||
<para>The type-level <classname>@SessionAttributes</classname>
|
||||
annotation declares session attributes used by a specific handler.
|
||||
|
@ -1786,7 +1759,49 @@ public class EditPetForm {
|
|||
</para>
|
||||
</note>
|
||||
</section>
|
||||
|
||||
<section id="mvc-ann-form-urlencoded-data">
|
||||
<title>Working with <literal>application/x-www-form-urlencoded</literal> data</title>
|
||||
|
||||
<para>The previous sections covered use of <interfacename>@ModelAttribute</interfacename>
|
||||
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 <literal>ServletRequest.getParameter*()</literal> family
|
||||
of methods to support form field access only for HTTP POST, not for HTTP PUT.
|
||||
</para>
|
||||
|
||||
<para>To support HTTP PUT requests, the <literal>spring-web</literal> module provides
|
||||
the filter <classname>HttpPutFormContentFilter</classname>, which can be configured
|
||||
in <filename>web.xml</filename>:</para>
|
||||
|
||||
<programlisting language="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>
|
||||
</programlisting>
|
||||
|
||||
<para>The above filter intercepts HTTP PUT requests with content type
|
||||
<literal>application/x-www-form-urlencoded</literal>, reads the form data from the
|
||||
body of the request, and wraps the <classname>ServletRequest</classname> in order
|
||||
to make the form data available through the <literal>ServletRequest.getParameter*()</literal>
|
||||
family of methods.</para>
|
||||
|
||||
</section>
|
||||
|
||||
<section id="mvc-ann-cookievalue">
|
||||
<title>Mapping cookie values with the @CookieValue annotation</title>
|
||||
|
||||
|
|
Loading…
Reference in New Issue