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.Enumeration;
  18. import java.util.HashMap;
  19. import java.util.Map;
  20. import javax.servlet.ServletException;
  21. import javax.servlet.http.HttpServletRequest;
  22. import javax.servlet.http.HttpServletResponse;
  23. import org.springframework.validation.BindException;
  24. import org.springframework.validation.Errors;
  25. import org.springframework.web.servlet.ModelAndView;
  26. import org.springframework.web.util.WebUtils;
  27. /**
  28. * Form controller for typical wizard-style workflows.
  29. *
  30. * <p>In contrast to classic forms, wizards have more than one form view page.
  31. * Therefore, there are various actions instead of one single submit action:
  32. * <ul>
  33. * <li>finish: trying to leave the wizard successfully, i.e. performing its
  34. * final action, and thus needing a valid state;
  35. * <li>cancel: leaving the wizard without performing its final action, and
  36. * thus without regard to the validity of its current state;
  37. * <li>page change: showing another wizard page, e.g. the next or previous
  38. * one, with regard to "dirty back" and "dirty forward".
  39. * </ul>
  40. *
  41. * <p>Finish and cancel actions can be triggered by request parameters, named
  42. * PARAM_FINISH ("_finish") and PARAM_CANCEL ("_cancel"), ignoring parameter
  43. * values to allow for HTML buttons. The target page for page changes can be
  44. * specified by PARAM_TARGET, appending the page number to the parameter name
  45. * (e.g. "_target1"). The action parameters are recognized when triggered by
  46. * image buttons too (via "_finish.x", "_abort.x", or "_target1.x").
  47. *
  48. * <p>The current page number will be stored in the session. It can also be
  49. * specified as request parameter PARAM_PAGE, to properly handle usage of
  50. * the back button in a browser: In this case, a submission always contains
  51. * the correct page number, even if the user submitted from an old view.
  52. *
  53. * <p>The page can only be changed if it validates correctly, except if a
  54. * "dirty back" or "dirty forward" is allowed. At finish, all pages get
  55. * validated again to guarantee a consistent state. Note that a validator's
  56. * default validate method is not executed when using this class! Rather,
  57. * the validatePage implementation should call special validateXXX methods
  58. * that the validator needs to provide, validating certain pieces of the
  59. * object. These can be combined to validate the elements of individual pages.
  60. *
  61. * <p>Note: Page numbering starts with 0, to be able to pass an array
  62. * consisting of the respective view names to setPages.
  63. *
  64. * @author Juergen Hoeller
  65. * @since 25.04.2003
  66. * @see #setPages
  67. * @see #validatePage
  68. * @see #processFinish
  69. * @see #processCancel
  70. */
  71. public abstract class AbstractWizardFormController extends AbstractFormController {
  72. /**
  73. * Parameter triggering the finish action.
  74. * Can be called from any wizard page!
  75. */
  76. public static final String PARAM_FINISH = "_finish";
  77. /**
  78. * Parameter triggering the cancel action.
  79. * Can be called from any wizard page!
  80. */
  81. public static final String PARAM_CANCEL = "_cancel";
  82. /**
  83. * Parameter specifying the target page,
  84. * appending the page number to the name.
  85. */
  86. public static final String PARAM_TARGET = "_target";
  87. /**
  88. * Parameter specifying the current page as value. Not necessary on
  89. * form pages, but allows to properly handle usage of the back button.
  90. * @see #setPageAttribute
  91. */
  92. public static final String PARAM_PAGE = "_page";
  93. private String[] pages;
  94. private String pageAttribute;
  95. private boolean allowDirtyBack = true;
  96. private boolean allowDirtyForward = false;
  97. /**
  98. * Create a new AbstractWizardFormController.
  99. */
  100. public AbstractWizardFormController() {
  101. // always needs session to keep data from all pages
  102. setSessionForm(true);
  103. // never validate everything on binding ->
  104. // wizards validate individual pages
  105. setValidateOnBinding(false);
  106. }
  107. /**
  108. * Set the wizard pages, i.e. the view names for the pages.
  109. * The array index is interpreted as page number.
  110. * @param pages view names for the pages
  111. */
  112. public final void setPages(String[] pages) {
  113. if (pages == null || pages.length == 0) {
  114. throw new IllegalArgumentException("No wizard pages defined");
  115. }
  116. this.pages = pages;
  117. }
  118. /**
  119. * Set the name of the page attribute in the model, containing
  120. * an Integer with the current page number.
  121. * <p>This will be necessary for single views rendering multiple view pages.
  122. * It also allows for specifying the optional "_page" parameter.
  123. * @param pageAttribute name of the page attribute
  124. * @see #PARAM_PAGE
  125. */
  126. public final void setPageAttribute(String pageAttribute) {
  127. this.pageAttribute = pageAttribute;
  128. }
  129. /**
  130. * Set if "dirty back" is allowed, i.e. if moving to a former wizard
  131. * page is allowed in case of validation errors for the current page.
  132. * @param allowDirtyBack if "dirty back" is allowed
  133. */
  134. public final void setAllowDirtyBack(boolean allowDirtyBack) {
  135. this.allowDirtyBack = allowDirtyBack;
  136. }
  137. /**
  138. * Set if "dirty forward" is allowed, i.e. if moving to a later wizard
  139. * page is allowed in case of validation errors for the current page.
  140. * @param allowDirtyForward if "dirty forward" is allowed
  141. */
  142. public final void setAllowDirtyForward(boolean allowDirtyForward) {
  143. this.allowDirtyForward = allowDirtyForward;
  144. }
  145. /**
  146. * Return the number of wizard pages.
  147. * Useful to check whether the last page has been reached.
  148. */
  149. protected final int getNrOfPages() {
  150. return this.pages.length;
  151. }
  152. /**
  153. * Call page-specific onBindAndValidate method.
  154. */
  155. protected final void onBindAndValidate(HttpServletRequest request, Object command, BindException errors)
  156. throws Exception {
  157. onBindAndValidate(request, command, errors, getCurrentPage(request));
  158. }
  159. /**
  160. * Callback for custom post-processing in terms of binding and validation.
  161. * Called on each submit, after standard binding but before page-specific
  162. * validation of this wizard form controller.
  163. * <p>Note: AbstractWizardFormController does not perform standand
  164. * validation on binding but rather applies page-specific validation
  165. * on processing the form submission.
  166. * @param request current HTTP request
  167. * @param command bound command
  168. * @param errors Errors instance for additional custom validation
  169. * @param page current wizard page
  170. * @throws Exception in case of invalid state or arguments
  171. * @see #bindAndValidate
  172. * @see #processFormSubmission
  173. * @see org.springframework.validation.Errors
  174. */
  175. protected void onBindAndValidate(HttpServletRequest request, Object command, BindException errors, int page)
  176. throws Exception {
  177. }
  178. /**
  179. * Call page-specific referenceData method.
  180. */
  181. protected final Map referenceData(HttpServletRequest request, Object command, Errors errors)
  182. throws Exception {
  183. return referenceData(request, command, errors, getCurrentPage(request));
  184. }
  185. /**
  186. * Create a reference data map for the given request, consisting of
  187. * bean name/bean instance pairs as expected by ModelAndView.
  188. * <p>Default implementation delegates to referenceData(HttpServletRequest, int).
  189. * Subclasses can override this to set reference data used in the view.
  190. * @param request current HTTP request
  191. * @param command form object with request parameters bound onto it
  192. * @param errors validation errors holder
  193. * @param page current wizard page
  194. * @return a Map with reference data entries, or null if none
  195. * @throws Exception in case of invalid state or arguments
  196. * @see #referenceData(HttpServletRequest, int)
  197. * @see ModelAndView
  198. */
  199. protected Map referenceData(HttpServletRequest request, Object command, Errors errors, int page)
  200. throws Exception {
  201. return referenceData(request, page);
  202. }
  203. /**
  204. * Create a reference data map for the given request, consisting of
  205. * bean name/bean instance pairs as expected by ModelAndView.
  206. * <p>Default implementation returns null.
  207. * Subclasses can override this to set reference data used in the view.
  208. * @param request current HTTP request
  209. * @param page current wizard page
  210. * @return a Map with reference data entries, or null if none
  211. * @throws Exception in case of invalid state or arguments
  212. * @see ModelAndView
  213. */
  214. protected Map referenceData(HttpServletRequest request, int page) throws Exception {
  215. return null;
  216. }
  217. /**
  218. * Show first page as form view.
  219. */
  220. protected final ModelAndView showForm(HttpServletRequest request, HttpServletResponse response, BindException errors)
  221. throws Exception {
  222. return showPage(request, errors, getInitialPage(request, errors.getTarget()));
  223. }
  224. /**
  225. * Prepare the form model and view, including reference and error data,
  226. * for the given page. Can be used in processFinish implementations,
  227. * to show the respective page in case of validation errors.
  228. * @param request current HTTP request
  229. * @param errors validation errors holder
  230. * @param page number of page to show
  231. * @return the prepared form view
  232. * @throws Exception in case of invalid state or arguments
  233. */
  234. protected final ModelAndView showPage(HttpServletRequest request, BindException errors, int page)
  235. throws Exception {
  236. if (page >= 0 && page < this.pages.length) {
  237. logger.debug("Showing wizard page " + page + " for form bean '" + getCommandName() + "'");
  238. // set page session attribute, expose overriding request attribute
  239. Integer pageInteger = new Integer(page);
  240. request.getSession().setAttribute(getPageSessionAttributeName(), pageInteger);
  241. request.setAttribute(getPageSessionAttributeName(), pageInteger);
  242. // set page request attribute for evaluation by views
  243. Map controlModel = new HashMap();
  244. if (this.pageAttribute != null) {
  245. controlModel.put(this.pageAttribute, new Integer(page));
  246. }
  247. return showForm(request, errors, this.pages[page], controlModel);
  248. }
  249. else {
  250. throw new ServletException("Invalid page number: " + page);
  251. }
  252. }
  253. /**
  254. * Return the initial page of the wizard, i.e. the page shown at wizard startup.
  255. * Default implementation delegates to getInitialPage(HttpServletRequest).
  256. * @param request current HTTP request
  257. * @param command the command object as returned by formBackingObject
  258. * @return the initial page number
  259. * @see #getInitialPage(HttpServletRequest)
  260. * @see #formBackingObject
  261. */
  262. protected int getInitialPage(HttpServletRequest request, Object command) {
  263. return getInitialPage(request);
  264. }
  265. /**
  266. * Return the initial page of the wizard, i.e. the page shown at wizard startup.
  267. * Default implementation returns 0 for first page.
  268. * @param request current HTTP request
  269. * @return the initial page number
  270. */
  271. protected int getInitialPage(HttpServletRequest request) {
  272. return 0;
  273. }
  274. /**
  275. * Return the name of the session attribute that holds
  276. * the page object for this controller.
  277. * @return the name of the page session attribute
  278. */
  279. protected final String getPageSessionAttributeName() {
  280. return getClass().getName() + ".page." + getCommandName();
  281. }
  282. /**
  283. * Handle an invalid submit request, e.g. when in session form mode but no form object
  284. * was found in the session (like in case of an invalid resubmit by the browser).
  285. * <p>Default implementation for wizard form controllers simply shows the initial page
  286. * of a new wizard form. If you want to show some "invalid submit" message, you need
  287. * to override this method.
  288. * @param request current HTTP request
  289. * @param response current HTTP response
  290. * @return a prepared view, or null if handled directly
  291. * @throws Exception in case of errors
  292. * @see #showNewForm
  293. * @see #setBindOnNewForm
  294. */
  295. protected ModelAndView handleInvalidSubmit(HttpServletRequest request, HttpServletResponse response)
  296. throws Exception {
  297. return showNewForm(request, response);
  298. }
  299. /**
  300. * Apply wizard workflow: finish, cancel, page change.
  301. */
  302. protected final ModelAndView processFormSubmission(HttpServletRequest request, HttpServletResponse response,
  303. Object command, BindException errors) throws Exception {
  304. int currentPage = getCurrentPage(request);
  305. // remove page session attribute, provide copy as request attribute
  306. request.getSession().removeAttribute(getPageSessionAttributeName());
  307. request.setAttribute(getPageSessionAttributeName(), new Integer(currentPage));
  308. // cancel?
  309. if (isCancel(request)) {
  310. logger.debug("Cancelling wizard for form bean '" + getCommandName() + "'");
  311. return processCancel(request, response, command, errors);
  312. }
  313. // finish?
  314. if (isFinish(request)) {
  315. logger.debug("Finishing wizard for form bean '" + getCommandName() + "'");
  316. return validatePagesAndFinish(request, response, command, errors, currentPage);
  317. }
  318. // normal submit: validate current page and show specified target page
  319. logger.debug("Validating wizard page " + currentPage + " for form bean '" + getCommandName() + "'");
  320. validatePage(command, errors, currentPage);
  321. int targetPage = getTargetPage(request, command, errors, currentPage);
  322. logger.debug("Target page " + targetPage + " requested");
  323. if (targetPage != currentPage) {
  324. if (!errors.hasErrors() || (this.allowDirtyBack && targetPage < currentPage) ||
  325. (this.allowDirtyForward && targetPage > currentPage)) {
  326. // allowed to go to target page
  327. return showPage(request, errors, targetPage);
  328. }
  329. }
  330. // show current page again
  331. return showPage(request, errors, currentPage);
  332. }
  333. /**
  334. * Return the current page number. Used by processFormSubmission.
  335. * <p>The default implementation checks the page session attribute.
  336. * Subclasses can override this for customized page determination.
  337. * @see #processFormSubmission
  338. * @see #getPageSessionAttributeName
  339. */
  340. protected int getCurrentPage(HttpServletRequest request) {
  341. // checking for overriding attribute in request
  342. Integer pageAttr = (Integer) request.getAttribute(getPageSessionAttributeName());
  343. if (pageAttr != null) {
  344. return pageAttr.intValue();
  345. }
  346. // check for explicit request parameter
  347. String pageParam = request.getParameter(PARAM_PAGE);
  348. if (pageParam != null) {
  349. return Integer.parseInt(pageParam);
  350. }
  351. // check for original attribute in session
  352. pageAttr = (Integer) request.getSession().getAttribute(getPageSessionAttributeName());
  353. if (pageAttr != null) {
  354. return pageAttr.intValue();
  355. }
  356. throw new IllegalStateException("Page attribute [" + getPageSessionAttributeName() +
  357. "] neither found in session nor in request");
  358. }
  359. /**
  360. * Return if finish action is specified in the request.
  361. * <p>Default implementation looks for "_finish" parameter in the request.
  362. * @param request current HTTP request
  363. * @see #PARAM_FINISH
  364. */
  365. protected boolean isFinish(HttpServletRequest request) {
  366. return WebUtils.hasSubmitParameter(request, PARAM_FINISH);
  367. }
  368. /**
  369. * Return if cancel action is specified in the request.
  370. * <p>Default implementation looks for "_cancel" parameter in the request.
  371. * @param request current HTTP request
  372. * @see #PARAM_CANCEL
  373. */
  374. protected boolean isCancel(HttpServletRequest request) {
  375. return WebUtils.hasSubmitParameter(request, PARAM_CANCEL);
  376. }
  377. /**
  378. * Return the target page specified in the request.
  379. * <p>Default implementation delegates to getTargetPage(HttpServletRequest, int).
  380. * Subclasses can override this for customized target page determination.
  381. * @param request current HTTP request
  382. * @param command form object with request parameters bound onto it
  383. * @param errors validation errors holder
  384. * @param currentPage the current page, to be returned as fallback
  385. * if no target page specified
  386. * @return the page specified in the request, or current page if not found
  387. * @see #getTargetPage(HttpServletRequest, int)
  388. */
  389. protected int getTargetPage(HttpServletRequest request, Object command, Errors errors, int currentPage) {
  390. return getTargetPage(request, currentPage);
  391. }
  392. /**
  393. * Return the target page specified in the request.
  394. * <p>Default implementation examines "_target" parameter (e.g. "_target1").
  395. * Subclasses can override this for customized target page determination.
  396. * @param request current HTTP request
  397. * @param currentPage the current page, to be returned as fallback
  398. * if no target page specified
  399. * @return the page specified in the request, or current page if not found
  400. * @see #PARAM_TARGET
  401. */
  402. protected int getTargetPage(HttpServletRequest request, int currentPage) {
  403. Enumeration paramNames = request.getParameterNames();
  404. while (paramNames.hasMoreElements()) {
  405. String paramName = (String) paramNames.nextElement();
  406. if (paramName.startsWith(PARAM_TARGET)) {
  407. for (int i = 0; i < WebUtils.SUBMIT_IMAGE_SUFFIXES.length; i++) {
  408. String suffix = WebUtils.SUBMIT_IMAGE_SUFFIXES[i];
  409. if (paramName.endsWith(suffix)) {
  410. paramName = paramName.substring(0, paramName.length() - suffix.length());
  411. }
  412. }
  413. return Integer.parseInt(paramName.substring(PARAM_TARGET.length()));
  414. }
  415. }
  416. return currentPage;
  417. }
  418. /**
  419. * Validate all pages and process finish.
  420. * If there are page validation errors, show the respective view page.
  421. */
  422. private ModelAndView validatePagesAndFinish(HttpServletRequest request, HttpServletResponse response,
  423. Object command, BindException errors, int currentPage)
  424. throws Exception {
  425. // in case of binding errors -> show current page
  426. if (errors.getErrorCount() - errors.getGlobalErrorCount() > 0) {
  427. return showPage(request, errors, currentPage);
  428. }
  429. // in case of field errors on a page -> show the page
  430. for (int page = 0; page < this.pages.length; page++) {
  431. validatePage(command, errors, page);
  432. if (errors.getErrorCount() - errors.getGlobalErrorCount() > 0) {
  433. return showPage(request, errors, page);
  434. }
  435. }
  436. // no field errors -> maybe global errors, or none at all
  437. return processFinish(request, response, command, errors);
  438. }
  439. /**
  440. * Template method for custom validation logic for individual pages.
  441. * <p>Implementations will typically call fine-granular validateXXX methods of this
  442. * instance's validator, combining them to validation of the respective pages.
  443. * The validator's default <code>validate</code> method will not be called by a
  444. * wizard form controller!
  445. * @param command form object with the current wizard state
  446. * @param errors validation errors holder
  447. * @param page number of page to validate
  448. * @see org.springframework.validation.Validator#validate
  449. */
  450. protected abstract void validatePage(Object command, Errors errors, int page);
  451. /**
  452. * Template method for processing the final action of this wizard.
  453. * <p>Call <code>errors.getModel()</code> to populate the ModelAndView model
  454. * with the command and the Errors instance, under the specified command name,
  455. * as expected by the "spring:bind" tag.
  456. * @param request current HTTP request
  457. * @param response current HTTP response
  458. * @param command form object with the current wizard state
  459. * @param errors validation errors holder
  460. * @return the finish view
  461. * @throws Exception in case of invalid state or arguments
  462. * @see org.springframework.validation.Errors
  463. * @see org.springframework.validation.BindException#getModel
  464. */
  465. protected abstract ModelAndView processFinish(HttpServletRequest request, HttpServletResponse response,
  466. Object command, BindException errors) throws Exception;
  467. /**
  468. * Template method for processing the cancel action of this wizard.
  469. * <p>Call <code>errors.getModel()</code> to populate the ModelAndView model
  470. * with the command and the Errors instance, under the specified command name,
  471. * as expected by the "spring:bind" tag.
  472. * @param request current HTTP request
  473. * @param response current HTTP response
  474. * @param command form object with the current wizard state
  475. * @param errors Errors instance containing errors
  476. * @return the cancellation view
  477. * @throws Exception in case of invalid state or arguments
  478. * @see org.springframework.validation.Errors
  479. * @see org.springframework.validation.BindException#getModel
  480. */
  481. protected abstract ModelAndView processCancel(HttpServletRequest request, HttpServletResponse response,
  482. Object command, BindException errors) throws Exception;
  483. }