1. /*
  2. * Copyright 2002-2004 the original author or authors.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package org.springframework.web.servlet.mvc;
  17. import java.util.Map;
  18. import javax.servlet.ServletException;
  19. import javax.servlet.http.HttpServletRequest;
  20. import javax.servlet.http.HttpServletResponse;
  21. import javax.servlet.http.HttpSession;
  22. import org.springframework.validation.BindException;
  23. import org.springframework.validation.Errors;
  24. import org.springframework.web.bind.ServletRequestDataBinder;
  25. import org.springframework.web.servlet.ModelAndView;
  26. /**
  27. * <p>Form controller that autopopulates a form bean from the request.
  28. * This, either using a new bean instance per request, or using the same bean
  29. * when the <code>sessionForm</code> property has been set to <code>true</code>.
  30. * This class is the base class for both framework subclasses like
  31. * {@link SimpleFormController SimpleFormController} and
  32. * {@link AbstractWizardFormController AbstractWizardFormController}, and
  33. * custom form controllers you can provide yourself.</p>
  34. * <p>Both form- input-views and after-submission-views have to be provided
  35. * programmatically. To provide those views using configuration properties,
  36. * use the {@link SimpleFormController SimpleFormController}.</p>
  37. *
  38. * <p>Subclasses need to override showForm to prepare the form view, and
  39. * processFormSubmission to handle submit requests. For the latter, binding errors
  40. * like type mismatches will be reported via the given "errors" holder.
  41. * For additional custom form validation, a validator (property inherited from
  42. * BaseCommandController) can be used, reporting via the same "errors" instance.</p>
  43. *
  44. * <p>Comparing this Controller to the Struts notion of the <code>Action</code>
  45. * shows us that with Spring, you can use any ordinary JavaBeans or database-
  46. * backed JavaBeans without having to implement a framework-specific class
  47. * (like Struts' <code>ActionForm</code>). More complex properties of JavaBeans
  48. * (Dates, Locales, but also your own application-specific or compound types)
  49. * can be represented and submitted to the controller, by using the notion of
  50. * <code>java.beans.PropertyEditors</code>. For more information on that
  51. * subject, see the workflow of this controller and the explanation of the
  52. * {@link BaseCommandController BaseCommandController}.</p>
  53. *
  54. * <p><b><a name="workflow">Workflow
  55. * (<a href="BaseCommandController.html#workflow">and that defined by superclass</a>):</b><br>
  56. * <ol>
  57. * <li><b>The controller receives a request for a new form (typically a GET).</b></li>
  58. * <li>Call to {@link #formBackingObject formBackingObject()} which by default,
  59. * returns an instance of the commandClass that has been configured
  60. * (see the properties the superclass exposes), but can also be overridden
  61. * to e.g. retrieve an object from the database (that needs to be modified
  62. * using the form).</li>
  63. * <li>Call to {@link #initBinder initBinder()} which allows you to register
  64. * custom editors for certain fields (often properties of non-primitive
  65. * or non-String types) of the command class. This will render appropriate
  66. * Strings for those property values, e.g. locale-specific date strings.</li>
  67. * <li>The {@link org.springframework.web.bind.ServletRequestDataBinder ServletRequestDataBinder}
  68. * gets applied to populate the new form object with initial request parameters.
  69. * (<i>only if <code>bindOnNewForm</code> is set to <code>true</code></i>)</li>
  70. * <li>Call to {@link #showForm(HttpServletRequest, HttpServletResponse, BindException) showForm()}
  71. * to return a View that should be rendered (typically the view that renders
  72. * the form). This method has to be implemented in subclasses.</li>
  73. * <li>Call to {@link #referenceData referenceData()} to allow you to bind any
  74. * relevant reference data you might need when editing a form (e.g. a List
  75. * of Locale objects you're going to let the user select one from)</li>
  76. * <li>Model gets exposed and view gets rendered, to let the user fill in the form.</li>
  77. * <li><b>The controller receives a form submission (typically a POST).</b>
  78. * To use a different way of detecting a form submission, override the
  79. * {@link #isFormSubmission isFormSubmission} method.
  80. * </li>
  81. * <li>If <code>sessionForm</code> is not set, {@link #formBackingObject formBackingObject()}
  82. * is called to retrieve a form object. Otherwise, the controller tries to
  83. * find the command object which is already bound in the session. If it cannot
  84. * find the object, it does a call to {@link #handleInvalidSubmit handleInvalidSubmit}
  85. * which - by default - tries to create a new form object and resubmit the form.</li>
  86. * <li>The {@link org.springframework.web.bind.ServletRequestDataBinder ServletRequestDataBinder}
  87. * gets applied to populate the form object with current request parameters.
  88. * <li>Call to {@link #onBind onBind(HttpServletRequest, Object, Errors)} which allows
  89. * you to do custom processing after binding but before validation (e.g. to manually
  90. * bind request parameters to bean properties, to be seen by the Validator).</li>
  91. * <li>If <code>validateOnBinding</code> is set, a registered Validator will be invoked.
  92. * The Validator will check the form object properties, and register corresponding
  93. * errors via the given {@link org.springframework.validation.Errors Errors}</li> object.
  94. * <li>Call to {@link #onBindAndValidate onBindAndValidate()} which allows you
  95. * to do custom processing after binding and validation (e.g. to manually
  96. * bind request parameters, and to validate them outside a Validator).</li>
  97. * <li>Call {@link #showForm(HttpServletRequest, HttpServletResponse, BindException) showForm()}
  98. * again if there have been any binding and/or validation errors. Else, call to
  99. * {@link #processFormSubmission processFormSubmission} to process the valid form
  100. * submission. This method has to be implemented in subclasses.</li>
  101. * </ol>
  102. * </p>
  103. *
  104. * <p>In session form mode, a submission without an existing form object in the
  105. * session is considered invalid, like in case of a resubmit/reload by the browser.
  106. * The {@link #handleInvalidSubmit handleInvalidSubmit} method is invoked then,
  107. * by default trying to resubmit. It can be overridden in subclasses to show
  108. * corresponding messages or to redirect to a new form, in order to avoid duplicate
  109. * submissions. The form object in the session can be considered a transaction
  110. * token in that case.</p>
  111. *
  112. * <p>Note that views should never retrieve form beans from the session but always
  113. * from the request, as prepared by the form controller. Remember that some view
  114. * technologies like Velocity cannot even access a HTTP session.</p>
  115. *
  116. * <p><b><a name="config">Exposed configuration properties</a>
  117. * (<a href="BaseCommandController.html#config">and those defined by superclass</a>):</b><br>
  118. * <table border="1">
  119. * <tr>
  120. * <td><b>name</b></td>
  121. * <td><b>default</b></td>
  122. * <td><b>description</b></td>
  123. * </tr>
  124. * <tr>
  125. * <td>bindOnNewForm</td>
  126. * <td>false</td>
  127. * <td>Indicates whether to bind servlet request parameters when
  128. * creating a new form. Otherwise, the parameters will only be
  129. * bound on form submission attempts.</td>
  130. * </tr>
  131. * <tr>
  132. * <td>sessionForm</td>
  133. * <td>false</td>
  134. * <td>Indicates whether the form object should be kept in the session
  135. * when a user asks for a new form. This allows you e.g. to retrieve
  136. * an object from the database, let the user edit it, and then persist
  137. * it again. Otherwise, a new command object will be created for each
  138. * request (even when showing the form again after validation errors).</td>
  139. * </tr>
  140. * </table>
  141. * </p>
  142. *
  143. * @author Rod Johnson
  144. * @author Juergen Hoeller
  145. * @author Alef Arendsen
  146. * @see SimpleFormController
  147. * @see AbstractWizardFormController
  148. */
  149. public abstract class AbstractFormController extends BaseCommandController {
  150. private boolean bindOnNewForm = false;
  151. private boolean sessionForm = false;
  152. /**
  153. * Create a new AbstractFormController.
  154. * <p>Subclasses should set the following properties, either in the constructor
  155. * or via a BeanFactory: commandName, commandClass, bindOnNewForm, sessionForm.
  156. * Note that commandClass doesn't need to be set when overriding
  157. * formBackingObject, as the latter determines the class anyway.
  158. * @see #setCommandName
  159. * @see #setCommandClass
  160. * @see #setBindOnNewForm
  161. * @see #setSessionForm
  162. * @see #formBackingObject
  163. */
  164. public AbstractFormController() {
  165. super();
  166. }
  167. /**
  168. * Set if request parameters should be bound to the form object
  169. * in case of a non-submitting request, i.e. a new form.
  170. */
  171. public final void setBindOnNewForm(boolean bindOnNewForm) {
  172. this.bindOnNewForm = bindOnNewForm;
  173. }
  174. /**
  175. * Return if request parameters should be bound in case of a new form.
  176. */
  177. protected final boolean isBindOnNewForm() {
  178. return bindOnNewForm;
  179. }
  180. /**
  181. * Activate resp. deactivate session form mode. In session form mode,
  182. * the form is stored in the session to keep the form object instance
  183. * between requests, instead of creating a new one on each request.
  184. * <p>This is necessary for either wizard-style controllers that populate a
  185. * single form object from multiple pages, or forms that populate a persistent
  186. * object that needs to be identical to allow for tracking changes.
  187. */
  188. public final void setSessionForm(boolean sessionForm) {
  189. this.sessionForm = sessionForm;
  190. }
  191. /**
  192. * Return if session form mode is activated.
  193. */
  194. protected final boolean isSessionForm() {
  195. return sessionForm;
  196. }
  197. /**
  198. * Return the name of the session attribute that holds
  199. * the form object for this controller.
  200. * @return the name of the form session attribute,
  201. * or null if not in session form mode.
  202. */
  203. protected final String getFormSessionAttributeName() {
  204. return isSessionForm() ? getClass().getName() + ".form." + getCommandName() : null;
  205. }
  206. /**
  207. * Handles two cases: form submissions and showing a new form.
  208. * Delegates the decision between the two to isFormSubmission,
  209. * always treating requests without existing form session attribute
  210. * as new form when using session form mode.
  211. */
  212. protected final ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
  213. throws Exception {
  214. if (isFormSubmission(request)) {
  215. if (isSessionForm() && request.getSession().getAttribute(getFormSessionAttributeName()) == null) {
  216. // cannot submit a session form if no form object is in the session
  217. return handleInvalidSubmit(request, response);
  218. }
  219. // process submit
  220. Object command = getCommand(request);
  221. ServletRequestDataBinder binder = bindAndValidate(request, command);
  222. return processFormSubmission(request, response, command, binder.getErrors());
  223. }
  224. else {
  225. return showNewForm(request, response);
  226. }
  227. }
  228. /**
  229. * Determine if the given request represents a form submission.
  230. * <p>Default implementation treats a POST request as form submission.
  231. * Note: If the form session attribute doesn't exist when using session form
  232. * mode, the request is always treated as new form by handleRequestInternal.
  233. * <p>Subclasses can override this to use a custom strategy, e.g. a specific
  234. * request parameter (assumably a hidden field or submit button name).
  235. * @param request current HTTP request
  236. * @return if the request represents a form submission
  237. */
  238. protected boolean isFormSubmission(HttpServletRequest request) {
  239. return "POST".equals(request.getMethod());
  240. }
  241. /**
  242. * Show a new form. Prepares a backing object for the current form
  243. * and the given request, including checking its validity.
  244. * @param request current HTTP request
  245. * @param response current HTTP response
  246. * @return the prepared form view
  247. * @throws Exception in case of an invalid new form object
  248. */
  249. protected final ModelAndView showNewForm(HttpServletRequest request, HttpServletResponse response)
  250. throws Exception {
  251. // show new form
  252. logger.debug("Displaying new form");
  253. Object formObject = formBackingObject(request);
  254. if (formObject == null) {
  255. throw new ServletException("Form object returned by formBackingObject() may not be null");
  256. }
  257. if (!checkCommand(formObject)) {
  258. throw new ServletException("Form object returned by formBackingObject() must match commandClass");
  259. }
  260. // bind without validation, to allow for prepopulating a form, and for
  261. // convenient error evaluation in views (on both first attempt and resubmit)
  262. ServletRequestDataBinder binder = createBinder(request, formObject);
  263. if (isBindOnNewForm()) {
  264. logger.debug("Binding to new form");
  265. binder.bind(request);
  266. }
  267. return showForm(request, response, binder.getErrors());
  268. }
  269. /**
  270. * Retrieve a backing object for the current form from the given request.
  271. * <p>The properties of the form object will correspond to the form field values
  272. * in your form view. This object will be exposed in the model under the specified
  273. * command name, to be accessed under that name in the view: for example, with
  274. * a "spring:bind" tag. The default command name is "command".
  275. * <p>Note that you need to activate session form mode to reuse the form-backing
  276. * object across the entire form workflow. Else, a new instance of the command
  277. * class will be created for each submission attempt, just using this backing
  278. * object as template for the initial form.
  279. * <p>Default implementation calls BaseCommandController.createCommand,
  280. * creating a new empty instance of the command class.
  281. * Subclasses can override this to provide a preinitialized backing object.
  282. * @param request current HTTP request
  283. * @return the backing objact
  284. * @throws Exception in case of invalid state or arguments
  285. * @see #setCommandName
  286. * @see #setCommandClass
  287. * @see #createCommand
  288. */
  289. protected Object formBackingObject(HttpServletRequest request) throws Exception {
  290. return createCommand();
  291. }
  292. /**
  293. * Prepare the form model and view, including reference and error data.
  294. * Can show a configured form page, or generate a programmatic form view.
  295. * <p>A typical implementation will call showForm(request,errors,"myView")
  296. * to prepare the form view for a specific view name.
  297. * <p>Note: If you decide to have a "formView" property specifying the
  298. * view name, consider using SimpleFormController.
  299. * @param request current HTTP request
  300. * @param response current HTTP response
  301. * @param errors validation errors holder
  302. * @return the prepared form view, or null if handled directly
  303. * @throws Exception in case of invalid state or arguments
  304. * @see #showForm(HttpServletRequest, BindException, String)
  305. * @see SimpleFormController#setFormView
  306. */
  307. protected abstract ModelAndView showForm(HttpServletRequest request, HttpServletResponse response,
  308. BindException errors) throws Exception;
  309. /**
  310. * Prepare model and view for the given form, including reference and errors.
  311. * In session form mode: Re-puts the form object in the session when returning
  312. * to the form, as it has been removed by getCommand.
  313. * Can be used in subclasses to redirect back to a specific form page.
  314. * @param request current HTTP request
  315. * @param errors validation errors holder
  316. * @param viewName name of the form view
  317. * @return the prepared form view
  318. * @throws Exception in case of invalid state or arguments
  319. */
  320. protected final ModelAndView showForm(HttpServletRequest request, BindException errors, String viewName)
  321. throws Exception {
  322. return showForm(request, errors, viewName, null);
  323. }
  324. /**
  325. * Prepare model and view for the given form, including reference and errors,
  326. * adding a controller-specific control model.
  327. * In session form mode: Re-puts the form object in the session when returning
  328. * to the form, as it has been removed by getCommand.
  329. * Can be used in subclasses to redirect back to a specific form page.
  330. * @param request current HTTP request
  331. * @param errors validation errors holder
  332. * @param viewName name of the form view
  333. * @param controlModel model map containing controller-specific control data
  334. * (e.g. current page in wizard-style controllers).
  335. * @return the prepared form view
  336. * @throws Exception in case of invalid state or arguments
  337. */
  338. protected final ModelAndView showForm(HttpServletRequest request, BindException errors, String viewName,
  339. Map controlModel) throws Exception {
  340. if (isSessionForm()) {
  341. request.getSession().setAttribute(getFormSessionAttributeName(), errors.getTarget());
  342. }
  343. Map model = errors.getModel();
  344. Map referenceData = referenceData(request, errors.getTarget(), errors);
  345. if (referenceData != null) {
  346. model.putAll(referenceData);
  347. }
  348. if (controlModel != null) {
  349. model.putAll(controlModel);
  350. }
  351. return new ModelAndView(viewName, model);
  352. }
  353. /**
  354. * Create a reference data map for the given request, consisting of
  355. * bean name/bean instance pairs as expected by ModelAndView.
  356. * <p>Default implementation returns null.
  357. * Subclasses can override this to set reference data used in the view.
  358. * @param request current HTTP request
  359. * @param command form object with request parameters bound onto it
  360. * @param errors validation errors holder
  361. * @return a Map with reference data entries, or null if none
  362. * @throws Exception in case of invalid state or arguments
  363. * @see ModelAndView
  364. */
  365. protected Map referenceData(HttpServletRequest request, Object command, Errors errors) throws Exception {
  366. return null;
  367. }
  368. /**
  369. * Handle an invalid submit request, e.g. when in session form mode but no form object
  370. * was found in the session (like in case of an invalid resubmit by the browser).
  371. * <p>Default implementation simply tries to resubmit the form with a new form object.
  372. * This should also work if the user hit the back button, changed some form data,
  373. * and resubmitted the form.
  374. * <p>Note: To avoid duplicate submissions, you need to override this method.
  375. * Either show some "invalid submit" message, or call showNewForm for resetting the
  376. * form (prepopulating it with the current values if "bindOnNewForm" is true).
  377. * In this case, the form object in the session serves as transaction token.
  378. * @param request current HTTP request
  379. * @param response current HTTP response
  380. * @return a prepared view, or null if handled directly
  381. * @throws Exception in case of errors
  382. * @see #showNewForm
  383. * @see #setBindOnNewForm
  384. */
  385. protected ModelAndView handleInvalidSubmit(HttpServletRequest request, HttpServletResponse response)
  386. throws Exception {
  387. Object command = formBackingObject(request);
  388. ServletRequestDataBinder binder = bindAndValidate(request, command);
  389. return processFormSubmission(request, response, command, binder.getErrors());
  390. }
  391. /**
  392. * Return the form object for the given request.
  393. * <p>Calls formBackingObject if not in session form mode. Else, retrieves the
  394. * form object from the session. Note that the form object gets removed from
  395. * the session, but it will be re-added when showing the form for resubmission.
  396. * @param request current HTTP request
  397. * @return object form to bind onto
  398. * @throws Exception in case of invalid state or arguments
  399. * @see #formBackingObject
  400. */
  401. protected final Object getCommand(HttpServletRequest request) throws Exception {
  402. if (!isSessionForm()) {
  403. return formBackingObject(request);
  404. }
  405. HttpSession session = request.getSession(false);
  406. if (session == null) {
  407. throw new ServletException("Must have session when trying to bind");
  408. }
  409. Object formObject = session.getAttribute(getFormSessionAttributeName());
  410. session.removeAttribute(getFormSessionAttributeName());
  411. if (formObject == null) {
  412. throw new ServletException("Form object not found in session");
  413. }
  414. return formObject;
  415. }
  416. /**
  417. * Process form submission request. Called by handleRequestInternal in case
  418. * of a form submission.
  419. * <p>Subclasses can override this to provide custom submission handling
  420. * like triggering a custom action. They can also provide custom validation
  421. * and call showForm or proceed with the submission accordingly.
  422. * <p>Call <code>errors.getModel()</code> to populate the ModelAndView model
  423. * with the command and the Errors instance, under the specified command name,
  424. * as expected by the "spring:bind" tag.
  425. * @param request current servlet request
  426. * @param response current servlet response
  427. * @param command form object with request parameters bound onto it
  428. * @param errors holder without errors (subclass can add errors if it wants to)
  429. * @return the prepared model and view, or null
  430. * @throws Exception in case of errors
  431. * @see #isFormSubmission
  432. * @see #showForm
  433. * @see org.springframework.validation.Errors
  434. * @see org.springframework.validation.BindException#getModel
  435. */
  436. protected abstract ModelAndView processFormSubmission(HttpServletRequest request, HttpServletResponse response,
  437. Object command, BindException errors)
  438. throws Exception;
  439. }