0001: /*
0002: * Copyright 2002-2005 the original author or authors.
0003: *
0004: * Licensed under the Apache License, Version 2.0 (the "License");
0005: * you may not use this file except in compliance with the License.
0006: * You may obtain a copy of the License at
0007: *
0008: * http://www.apache.org/licenses/LICENSE-2.0
0009: *
0010: * Unless required by applicable law or agreed to in writing, software
0011: * distributed under the License is distributed on an "AS IS" BASIS,
0012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0013: * See the License for the specific language governing permissions and
0014: * limitations under the License.
0015: */
0016:
0017: package org.springframework.web.portlet.mvc;
0018:
0019: import java.util.Enumeration;
0020: import java.util.HashMap;
0021: import java.util.Iterator;
0022: import java.util.Map;
0023:
0024: import javax.portlet.ActionRequest;
0025: import javax.portlet.ActionResponse;
0026: import javax.portlet.PortletException;
0027: import javax.portlet.PortletRequest;
0028: import javax.portlet.RenderRequest;
0029: import javax.portlet.RenderResponse;
0030:
0031: import org.springframework.validation.BindException;
0032: import org.springframework.validation.Errors;
0033: import org.springframework.web.portlet.ModelAndView;
0034: import org.springframework.web.portlet.util.PortletUtils;
0035: import org.springframework.web.util.WebUtils;
0036:
0037: /**
0038: * Form controller for typical wizard-style workflows.
0039: *
0040: * <p>In contrast to classic forms, wizards have more than one form view page.
0041: * Therefore, there are various actions instead of one single submit action:
0042: * <ul>
0043: * <li>finish: trying to leave the wizard successfully, i.e. performing its
0044: * final action, and thus needing a valid state;
0045: * <li>cancel: leaving the wizard without performing its final action, and
0046: * thus without regard to the validity of its current state;
0047: * <li>page change: showing another wizard page, e.g. the next or previous
0048: * one, with regard to "dirty back" and "dirty forward".
0049: * </ul>
0050: *
0051: * <p>Finish and cancel actions can be triggered by request parameters, named
0052: * PARAM_FINISH ("_finish") and PARAM_CANCEL ("_cancel"), ignoring parameter
0053: * values to allow for HTML buttons. The target page for page changes can be
0054: * specified by PARAM_TARGET, appending the page number to the parameter name
0055: * (e.g. "_target1"). The action parameters are recognized when triggered by
0056: * image buttons too (via "_finish.x", "_abort.x", or "_target1.x").
0057: *
0058: * <p>The current page number will be stored in the session. It can also be
0059: * specified as request parameter PARAM_PAGE, to properly handle usage of
0060: * the back button in a browser: In this case, a submission always contains
0061: * the correct page number, even if the user submitted from an old view.
0062: *
0063: * <p>The page can only be changed if it validates correctly, except if a
0064: * "dirty back" or "dirty forward" is allowed. At finish, all pages get
0065: * validated again to guarantee a consistent state.
0066: *
0067: * <p>Note that a validator's default validate method is not executed when using
0068: * this class! Rather, the <code>validatePage</code> implementation should call
0069: * special <code>validateXXX</code> methods that the validator needs to provide,
0070: * validating certain pieces of the object. These can be combined to validate
0071: * the elements of individual pages.
0072: *
0073: * <p>Note: Page numbering starts with 0, to be able to pass an array
0074: * consisting of the corresponding view names to the "pages" bean property.
0075: *
0076: * <p>Parameters indicated with <code>setPassRenderParameters</code> will be present
0077: * for each page. If there are render parameters you need in <code>renderFinish</code>
0078: * or <code>renderCancel</code>, then you need to pass those forward from the
0079: * <code>processFinish</code> or <code>processCancel</code> methods, respectively.
0080:
0081: * @author Juergen Hoeller
0082: * @author John A. Lewis
0083: * @since 2.0
0084: * @see #setPages
0085: * @see #validatePage
0086: * @see #processFinish
0087: * @see #processCancel
0088: */
0089: public abstract class AbstractWizardFormController extends
0090: AbstractFormController {
0091:
0092: /**
0093: * Parameter triggering the finish action.
0094: * Can be called from any wizard page!
0095: */
0096: public static final String PARAM_FINISH = "_finish";
0097:
0098: /**
0099: * Parameter triggering the cancel action.
0100: * Can be called from any wizard page!
0101: */
0102: public static final String PARAM_CANCEL = "_cancel";
0103:
0104: /**
0105: * Parameter specifying the target page,
0106: * appending the page number to the name.
0107: */
0108: public static final String PARAM_TARGET = "_target";
0109:
0110: /**
0111: * Parameter specifying the current page as value. Not necessary on
0112: * form pages, but allows to properly handle usage of the back button.
0113: * @see #setPageAttribute
0114: */
0115: public static final String PARAM_PAGE = "_page";
0116:
0117: private String[] pages;
0118:
0119: private String pageAttribute;
0120:
0121: private boolean allowDirtyBack = true;
0122:
0123: private boolean allowDirtyForward = false;
0124:
0125: /**
0126: * Create a new AbstractWizardFormController.
0127: * <p>"sessionForm" is automatically turned on, "validateOnBinding"
0128: * turned off, and "cacheSeconds" set to 0 by the base class
0129: * (-> no caching for all form controllers).
0130: */
0131: public AbstractWizardFormController() {
0132: // AbstractFormController sets default cache seconds to 0.
0133: super ();
0134:
0135: // Always needs session to keep data from all pages.
0136: setSessionForm(true);
0137:
0138: // Never validate everything on binding ->
0139: // wizards validate individual pages.
0140: setValidateOnBinding(false);
0141: }
0142:
0143: /**
0144: * Set the wizard pages, i.e. the view names for the pages.
0145: * The array index is interpreted as page number.
0146: * @param pages view names for the pages
0147: */
0148: public final void setPages(String[] pages) {
0149: if (pages == null || pages.length == 0) {
0150: throw new IllegalArgumentException(
0151: "No wizard pages defined");
0152: }
0153: this .pages = pages;
0154: }
0155:
0156: /**
0157: * Return the wizard pages, i.e. the view names for the pages.
0158: * The array index corresponds to the page number.
0159: * <p>Note that a concrete wizard form controller might override
0160: * <code>getViewName(PortletRequest, Object, int)</code> to
0161: * determine the view name for each page dynamically.
0162: * @see #getViewName(PortletRequest, Object, int)
0163: */
0164: public final String[] getPages() {
0165: return pages;
0166: }
0167:
0168: /**
0169: * Return the number of wizard pages.
0170: * Useful to check whether the last page has been reached.
0171: * <p>Note that a concrete wizard form controller might override
0172: * <code>getPageCount(PortletRequest, Object)</code> to determine
0173: * the page count dynamically.
0174: * @see #getPageCount(PortletRequest, Object)
0175: */
0176: protected final int getPageCount() {
0177: return this .pages.length;
0178: }
0179:
0180: /**
0181: * Set the name of the page attribute in the model, containing
0182: * an Integer with the current page number.
0183: * <p>This will be necessary for single views rendering multiple view pages.
0184: * It also allows for specifying the optional "_page" parameter.
0185: * @param pageAttribute name of the page attribute
0186: * @see #PARAM_PAGE
0187: */
0188: public final void setPageAttribute(String pageAttribute) {
0189: this .pageAttribute = pageAttribute;
0190: }
0191:
0192: /**
0193: * Return the name of the page attribute in the model.
0194: */
0195: public final String getPageAttribute() {
0196: return pageAttribute;
0197: }
0198:
0199: /**
0200: * Set if "dirty back" is allowed, i.e. if moving to a former wizard
0201: * page is allowed in case of validation errors for the current page.
0202: * @param allowDirtyBack if "dirty back" is allowed
0203: */
0204: public final void setAllowDirtyBack(boolean allowDirtyBack) {
0205: this .allowDirtyBack = allowDirtyBack;
0206: }
0207:
0208: /**
0209: * Return whether "dirty back" is allowed.
0210: */
0211: public final boolean isAllowDirtyBack() {
0212: return allowDirtyBack;
0213: }
0214:
0215: /**
0216: * Set if "dirty forward" is allowed, i.e. if moving to a later wizard
0217: * page is allowed in case of validation errors for the current page.
0218: * @param allowDirtyForward if "dirty forward" is allowed
0219: */
0220: public final void setAllowDirtyForward(boolean allowDirtyForward) {
0221: this .allowDirtyForward = allowDirtyForward;
0222: }
0223:
0224: /**
0225: * Return whether "dirty forward" is allowed.
0226: */
0227: public final boolean isAllowDirtyForward() {
0228: return allowDirtyForward;
0229: }
0230:
0231: /**
0232: * Calls page-specific onBindAndValidate method.
0233: */
0234: protected final void onBindAndValidate(PortletRequest request,
0235: Object command, BindException errors) throws Exception {
0236:
0237: onBindAndValidate(request, command, errors,
0238: getCurrentPage(request));
0239: }
0240:
0241: /**
0242: * Callback for custom post-processing in terms of binding and validation.
0243: * Called on each submit, after standard binding but before page-specific
0244: * validation of this wizard form controller.
0245: * <p>Note: AbstractWizardFormController does not perform standard
0246: * validation on binding but rather applies page-specific validation
0247: * on processing the form submission.
0248: * @param request current portlet request
0249: * @param command bound command
0250: * @param errors Errors instance for additional custom validation
0251: * @param page current wizard page
0252: * @throws Exception in case of invalid state or arguments
0253: * @see #bindAndValidate
0254: * @see #processFormSubmission
0255: * @see org.springframework.validation.Errors
0256: */
0257: protected void onBindAndValidate(PortletRequest request,
0258: Object command, BindException errors, int page)
0259: throws Exception {
0260: }
0261:
0262: /**
0263: * Consider an explicit finish or cancel request as a form submission too.
0264: * @see #isFinishRequest(PortletRequest)
0265: * @see #isCancelRequest(PortletRequest)
0266: */
0267: protected boolean isFormSubmission(PortletRequest request) {
0268: return super .isFormSubmission(request)
0269: || isFinishRequest(request) || isCancelRequest(request);
0270: }
0271:
0272: /**
0273: * Calls page-specific referenceData method.
0274: */
0275: protected final Map referenceData(PortletRequest request,
0276: Object command, Errors errors) throws Exception {
0277:
0278: return referenceData(request, command, errors,
0279: getCurrentPage(request));
0280: }
0281:
0282: /**
0283: * Create a reference data map for the given request, consisting of
0284: * bean name/bean instance pairs as expected by ModelAndView.
0285: * <p>Default implementation delegates to referenceData(HttpServletRequest, int).
0286: * Subclasses can override this to set reference data used in the view.
0287: * @param request current portlet request
0288: * @param command form object with request parameters bound onto it
0289: * @param errors validation errors holder
0290: * @param page current wizard page
0291: * @return a Map with reference data entries, or null if none
0292: * @throws Exception in case of invalid state or arguments
0293: * @see #referenceData(PortletRequest, int)
0294: * @see org.springframework.web.portlet.ModelAndView
0295: */
0296: protected Map referenceData(PortletRequest request, Object command,
0297: Errors errors, int page) throws Exception {
0298:
0299: return referenceData(request, page);
0300: }
0301:
0302: /**
0303: * Create a reference data map for the given request, consisting of
0304: * bean name/bean instance pairs as expected by ModelAndView.
0305: * <p>Default implementation returns null.
0306: * Subclasses can override this to set reference data used in the view.
0307: * @param request current portlet request
0308: * @param page current wizard page
0309: * @return a Map with reference data entries, or null if none
0310: * @throws Exception in case of invalid state or arguments
0311: * @see org.springframework.web.portlet.ModelAndView
0312: */
0313: protected Map referenceData(PortletRequest request, int page)
0314: throws Exception {
0315: return null;
0316: }
0317:
0318: /**
0319: * Show first page as form view.
0320: */
0321: protected final ModelAndView showForm(RenderRequest request,
0322: RenderResponse response, BindException errors)
0323: throws Exception {
0324:
0325: return showPage(request, errors, getInitialPage(request, errors
0326: .getTarget()));
0327: }
0328:
0329: /**
0330: * Prepare the form model and view, including reference and error data,
0331: * for the given page. Can be used in <code>processFinish</code> implementations,
0332: * to show the corresponding page in case of validation errors.
0333: * @param request current portlet render request
0334: * @param errors validation errors holder
0335: * @param page number of page to show
0336: * @return the prepared form view
0337: * @throws Exception in case of invalid state or arguments
0338: */
0339: protected final ModelAndView showPage(RenderRequest request,
0340: BindException errors, int page) throws Exception {
0341:
0342: if (page >= 0
0343: && page < getPageCount(request, errors.getTarget())) {
0344: if (logger.isDebugEnabled()) {
0345: logger.debug("Showing wizard page " + page
0346: + " for form bean '" + getCommandName() + "'");
0347: }
0348:
0349: // Set page session attribute, expose overriding request attribute.
0350: Integer pageInteger = new Integer(page);
0351: String pageAttrName = getPageSessionAttributeName(request);
0352: if (isSessionForm()) {
0353: if (logger.isDebugEnabled()) {
0354: logger.debug("Setting page session attribute ["
0355: + pageAttrName + "] to: " + pageInteger);
0356: }
0357: request.getPortletSession().setAttribute(pageAttrName,
0358: pageInteger);
0359: }
0360: request.setAttribute(pageAttrName, pageInteger);
0361:
0362: // Set page request attribute for evaluation by views.
0363: Map controlModel = new HashMap();
0364: if (this .pageAttribute != null) {
0365: controlModel.put(this .pageAttribute, new Integer(page));
0366: }
0367: String viewName = getViewName(request, errors.getTarget(),
0368: page);
0369: return showForm(request, errors, viewName, controlModel);
0370: }
0371:
0372: else {
0373: throw new PortletException("Invalid wizard page number: "
0374: + page);
0375: }
0376: }
0377:
0378: /**
0379: * Return the page count for this wizard form controller.
0380: * Default implementation delegates to <code>getPageCount()</code>.
0381: * <p>Can be overridden to dynamically adapt the page count.
0382: * @param request current portlet request
0383: * @param command the command object as returned by formBackingObject
0384: * @return the current page count
0385: * @see #getPageCount
0386: */
0387: protected int getPageCount(PortletRequest request, Object command) {
0388: return getPageCount();
0389: }
0390:
0391: /**
0392: * Return the name of the view for the specified page of this wizard form controller.
0393: * Default implementation takes the view name from the <code>getPages()</code> array.
0394: * <p>Can be overridden to dynamically switch the page view or to return view names
0395: * for dynamically defined pages.
0396: * @param request current portlet request
0397: * @param command the command object as returned by formBackingObject
0398: * @return the current page count
0399: * @see #getPageCount
0400: */
0401: protected String getViewName(PortletRequest request,
0402: Object command, int page) {
0403: return getPages()[page];
0404: }
0405:
0406: /**
0407: * Return the initial page of the wizard, i.e. the page shown at wizard startup.
0408: * Default implementation delegates to <code>getInitialPage(PortletRequest)</code>.
0409: * @param request current portlet request
0410: * @param command the command object as returned by formBackingObject
0411: * @return the initial page number
0412: * @see #getInitialPage(PortletRequest)
0413: * @see #formBackingObject
0414: */
0415: protected int getInitialPage(PortletRequest request, Object command) {
0416: return getInitialPage(request);
0417: }
0418:
0419: /**
0420: * Return the initial page of the wizard, i.e. the page shown at wizard startup.
0421: * Default implementation returns 0 for first page.
0422: * @param request current portlet request
0423: * @return the initial page number
0424: */
0425: protected int getInitialPage(PortletRequest request) {
0426: return 0;
0427: }
0428:
0429: /**
0430: * Return the name of the PortletSession attribute that holds the page object
0431: * for this wizard form controller.
0432: * <p>Default implementation delegates to the <code>getPageSessionAttributeName</code>
0433: * version without arguments.
0434: * @param request current portlet request
0435: * @return the name of the form session attribute, or null if not in session form mode
0436: * @see #getPageSessionAttributeName
0437: * @see #getFormSessionAttributeName
0438: * @see javax.portlet.PortletSession#getAttribute
0439: */
0440: protected String getPageSessionAttributeName(PortletRequest request) {
0441: return getPageSessionAttributeName();
0442: }
0443:
0444: /**
0445: * Return the name of the PortletSession attribute that holds the page object
0446: * for this wizard form controller.
0447: * <p>Default is an internal name, of no relevance to applications, as the form
0448: * session attribute is not usually accessed directly. Can be overridden to use
0449: * an application-specific attribute name, which allows other code to access
0450: * the session attribute directly.
0451: * @return the name of the page session attribute
0452: * @see #getFormSessionAttributeName
0453: * @see javax.portlet.PortletSession#getAttribute
0454: */
0455: protected String getPageSessionAttributeName() {
0456: return getClass().getName() + ".PAGE." + getCommandName();
0457: }
0458:
0459: /**
0460: * Pass the page number to the render phase by setting a render parameter.
0461: * This method may not be called when the action calls
0462: * {@link javax.portlet.ActionResponse#sendRedirect(String)}.
0463: * @param response the current action response
0464: * @param page the page number
0465: * @see ActionResponse#setRenderParameter
0466: */
0467: protected void setPageRenderParameter(ActionResponse response,
0468: int page) {
0469: if (logger.isDebugEnabled())
0470: logger.debug("Setting page number render parameter ["
0471: + PARAM_PAGE + "] to [" + page + "]");
0472: try {
0473: response.setRenderParameter(PARAM_PAGE, new Integer(page)
0474: .toString());
0475: } catch (IllegalStateException ex) {
0476: // ignore in case sendRedirect was already set
0477: }
0478: }
0479:
0480: /**
0481: * Pass the the parameter that indicates the target page of the request
0482: * forward to the render phase. If the <code>getTargetPage<code> method
0483: * was overridden, this may need to be overriden as well.
0484: * @param request the current action request
0485: * @param response the current action response
0486: * @see #PARAM_TARGET
0487: * @see #getTargetPage(PortletRequest, int)
0488: * @see #getTargetPage(PortletRequest, Object, Errors, int)
0489: * @see ActionResponse#setRenderParameter
0490: */
0491: protected void setTargetRenderParameter(ActionRequest request,
0492: ActionResponse response) {
0493: try {
0494: Iterator it = PortletUtils.getParametersStartingWith(
0495: request, PARAM_TARGET).entrySet().iterator();
0496: while (it.hasNext()) {
0497: Map.Entry entry = (Map.Entry) it.next();
0498: String param = PARAM_TARGET + (String) entry.getKey();
0499: Object value = entry.getValue();
0500: if (logger.isDebugEnabled())
0501: logger.debug("Setting target render parameter ["
0502: + param + "]");
0503: if (value instanceof String)
0504: response.setRenderParameter(param, (String) value);
0505: if (value instanceof String[])
0506: response
0507: .setRenderParameter(param, (String[]) value);
0508: }
0509: } catch (IllegalStateException ex) {
0510: // ignore in case sendRedirect was already set
0511: }
0512: }
0513:
0514: /**
0515: * Pass the the parameter that indicates a finish request forward to the
0516: * render phase. If the <code>isFinishRequest</code> method
0517: * was overridden, this may need to be overriden as well.
0518: * @param request the current action request
0519: * @param response the current action response
0520: * @see #PARAM_FINISH
0521: * @see #isFinishRequest
0522: * @see ActionResponse#setRenderParameter
0523: */
0524: protected void setFinishRenderParameter(ActionRequest request,
0525: ActionResponse response) {
0526: if (logger.isDebugEnabled())
0527: logger.debug("Setting cancel render parameter ["
0528: + PARAM_FINISH + "]");
0529: try {
0530: String name = PortletUtils.getSubmitParameter(request,
0531: PARAM_FINISH);
0532: if (name != null)
0533: response.setRenderParameter(name, request
0534: .getParameter(name));
0535: } catch (IllegalStateException ex) {
0536: // ignore in case sendRedirect was already set
0537: }
0538: }
0539:
0540: /**
0541: * Pass the the parameter that indicates a cancel request forward to the
0542: * render phase. If the <code>isCancelRequest</code> method
0543: * was overridden, this may need to be overriden as well.
0544: * @param request the current action request
0545: * @param response the current action response
0546: * @see #PARAM_CANCEL
0547: * @see #isCancelRequest
0548: * @see ActionResponse#setRenderParameter
0549: */
0550: protected void setCancelRenderParameter(ActionRequest request,
0551: ActionResponse response) {
0552: if (logger.isDebugEnabled())
0553: logger.debug("Setting cancel render parameter ["
0554: + PARAM_CANCEL + "]");
0555: try {
0556: String name = PortletUtils.getSubmitParameter(request,
0557: PARAM_CANCEL);
0558: if (name != null)
0559: response.setRenderParameter(name, request
0560: .getParameter(name));
0561: } catch (IllegalStateException ex) {
0562: // ignore in case sendRedirect was already set
0563: }
0564: }
0565:
0566: /**
0567: * Handle an invalid submit request, e.g. when in session form mode but no form object
0568: * was found in the session (like in case of an invalid resubmit by the browser).
0569: * <p>Default implementation for wizard form controllers simply shows the initial page
0570: * of a new wizard form. If you want to show some "invalid submit" message, you need
0571: * to override this method.
0572: * @param request current portlet render request
0573: * @param response current portlet render response
0574: * @return a prepared view, or null if handled directly
0575: * @throws Exception in case of errors
0576: * @see #showNewForm
0577: * @see #setBindOnNewForm
0578: * @see #handleInvalidSubmit
0579: */
0580: protected ModelAndView renderInvalidSubmit(RenderRequest request,
0581: RenderResponse response) throws Exception {
0582:
0583: return showNewForm(request, response);
0584: }
0585:
0586: /**
0587: * Handle an invalid submit request, e.g. when in session form mode but no form object
0588: * was found in the session (like in case of an invalid resubmit by the browser).
0589: * <p>Default implementation for wizard form controllers simply shows the initial page
0590: * of a new wizard form, so here in the action phase this method does nothing. If you
0591: * want to take some action on an invalid submit, you need to override this method.
0592: * @param request current portlet action request
0593: * @param response current portlet action response
0594: * @throws Exception in case of errors
0595: * @see #renderInvalidSubmit
0596: */
0597: protected void handleInvalidSubmit(ActionRequest request,
0598: ActionResponse response) throws Exception {
0599: }
0600:
0601: /**
0602: * Apply wizard workflow: finish, cancel, page change.
0603: * @see #processFormSubmission
0604: */
0605: protected final ModelAndView renderFormSubmission(
0606: RenderRequest request, RenderResponse response,
0607: Object command, BindException errors) throws Exception {
0608:
0609: int currentPage = getCurrentPage(request);
0610: String pageAttrName = getPageSessionAttributeName(request);
0611: request.setAttribute(pageAttrName, new Integer(currentPage));
0612:
0613: // cancel?
0614: if (isCancelRequest(request)) {
0615: if (logger.isDebugEnabled()) {
0616: logger.debug("Cancelling wizard for form bean '"
0617: + getCommandName() + "'");
0618: }
0619: return renderCancel(request, response, command, errors);
0620: }
0621:
0622: // finish?
0623: if (isFinishRequest(request)) {
0624: if (logger.isDebugEnabled()) {
0625: logger.debug("Finishing wizard for form bean '"
0626: + getCommandName() + "'");
0627: }
0628: return renderValidatePagesAndFinish(request, response,
0629: command, errors, currentPage);
0630: }
0631:
0632: // Normal submit: show specified target page.
0633: int targetPage = getTargetPage(request, command, errors,
0634: currentPage);
0635: if (logger.isDebugEnabled()) {
0636: logger.debug("Target page " + targetPage + " requested");
0637: }
0638: if (targetPage != currentPage) {
0639: if (!errors.hasErrors()
0640: || (this .allowDirtyBack && targetPage < currentPage)
0641: || (this .allowDirtyForward && targetPage > currentPage)) {
0642: // Allowed to go to target page.
0643: return showPage(request, errors, targetPage);
0644: }
0645: }
0646:
0647: // Show current page again
0648: return showPage(request, errors, currentPage);
0649: }
0650:
0651: /**
0652: * Apply wizard workflow: finish, cancel, page change.
0653: * @see #renderFormSubmission
0654: */
0655: protected final void processFormSubmission(ActionRequest request,
0656: ActionResponse response, Object command,
0657: BindException errors) throws Exception {
0658:
0659: int currentPage = getCurrentPage(request);
0660: // Remove page session attribute, provide copy as request attribute.
0661: String pageAttrName = getPageSessionAttributeName(request);
0662: if (isSessionForm()) {
0663: if (logger.isDebugEnabled()) {
0664: logger.debug("Removing page session attribute ["
0665: + pageAttrName + "]");
0666: }
0667: request.getPortletSession().removeAttribute(pageAttrName);
0668: }
0669: request.setAttribute(pageAttrName, new Integer(currentPage));
0670:
0671: // cancel?
0672: if (isCancelRequest(request)) {
0673: if (logger.isDebugEnabled()) {
0674: logger.debug("Cancelling wizard for form bean '"
0675: + getCommandName() + "'");
0676: }
0677: setPageRenderParameter(response, currentPage);
0678: setCancelRenderParameter(request, response);
0679: processCancel(request, response, command, errors);
0680: return;
0681: }
0682:
0683: // finish?
0684: if (isFinishRequest(request)) {
0685: if (logger.isDebugEnabled()) {
0686: logger.debug("Finishing wizard for form bean '"
0687: + getCommandName() + "'");
0688: }
0689: if (!isRedirectAction()) {
0690: setPageRenderParameter(response, currentPage);
0691: setFinishRenderParameter(request, response);
0692: }
0693: validatePagesAndFinish(request, response, command, errors,
0694: currentPage);
0695: return;
0696: }
0697:
0698: // Normal submit: validate current page
0699: if (!suppressValidation(request)) {
0700: if (logger.isDebugEnabled()) {
0701: logger.debug("Validating wizard page " + currentPage
0702: + " for form bean '" + getCommandName() + "'");
0703: }
0704: validatePage(command, errors, currentPage, false);
0705: }
0706:
0707: setPageRenderParameter(response, currentPage);
0708: setTargetRenderParameter(request, response);
0709: passRenderParameters(request, response);
0710:
0711: // Give subclasses a change to perform custom post-procession
0712: // of the current page and its command object.
0713: postProcessPage(request, command, errors, currentPage);
0714:
0715: }
0716:
0717: /**
0718: * Return the current page number. Used by processFormSubmission.
0719: * <p>The default implementation checks the page session attribute.
0720: * Subclasses can override this for customized page determination.
0721: * @see #processFormSubmission
0722: * @see #getPageSessionAttributeName
0723: */
0724: protected int getCurrentPage(PortletRequest request) {
0725: // Check for overriding attribute in request.
0726: String pageAttrName = getPageSessionAttributeName(request);
0727: Integer pageAttr = (Integer) request.getAttribute(pageAttrName);
0728: if (pageAttr != null) {
0729: return pageAttr.intValue();
0730: }
0731: // Check for explicit request parameter.
0732: String pageParam = request.getParameter(PARAM_PAGE);
0733: if (pageParam != null) {
0734: return Integer.parseInt(pageParam);
0735: }
0736: // Check for original attribute in session.
0737: if (isSessionForm()) {
0738: pageAttr = (Integer) request.getPortletSession()
0739: .getAttribute(pageAttrName);
0740: if (pageAttr != null) {
0741: return pageAttr.intValue();
0742: }
0743: }
0744: throw new IllegalStateException("Page attribute ["
0745: + pageAttrName
0746: + "] neither found in session nor in request");
0747: }
0748:
0749: /**
0750: * Determine whether the incoming request is a request to finish the
0751: * processing of the current form.
0752: * <p>By default, this method returns <code>true</code> if a parameter
0753: * matching the "_finish" key is present in the request, otherwise it
0754: * returns <code>false</code>. Subclasses may override this method
0755: * to provide custom logic to detect a finish request.
0756: * <p>The parameter is recognized both when sent as a plain parameter
0757: * ("_finish") or when triggered by an image button ("_finish.x").
0758: * @param request current portlet request
0759: * @see #PARAM_FINISH
0760: */
0761: protected boolean isFinishRequest(PortletRequest request) {
0762: return PortletUtils.hasSubmitParameter(request, PARAM_FINISH);
0763: }
0764:
0765: /**
0766: * Determine whether the incoming request is a request to cancel the
0767: * processing of the current form.
0768: * <p>By default, this method returns <code>true</code> if a parameter
0769: * matching the "_cancel" key is present in the request, otherwise it
0770: * returns <code>false</code>. Subclasses may override this method
0771: * to provide custom logic to detect a cancel request.
0772: * <p>The parameter is recognized both when sent as a plain parameter
0773: * ("_cancel") or when triggered by an image button ("_cancel.x").
0774: * @param request current portlet request
0775: * @see #PARAM_CANCEL
0776: */
0777: protected boolean isCancelRequest(PortletRequest request) {
0778: return PortletUtils.hasSubmitParameter(request, PARAM_CANCEL);
0779: }
0780:
0781: /**
0782: * Return the target page specified in the request.
0783: * <p>Default implementation delegates to
0784: * <code>getTargetPage(PortletRequest, int)</code>.
0785: * Subclasses can override this for customized target page determination.
0786: * @param request current portlet request
0787: * @param command form object with request parameters bound onto it
0788: * @param errors validation errors holder
0789: * @param currentPage the current page, to be returned as fallback
0790: * if no target page specified
0791: * @return the page specified in the request, or current page if not found
0792: * @see #getTargetPage(PortletRequest, int)
0793: */
0794: protected int getTargetPage(PortletRequest request, Object command,
0795: Errors errors, int currentPage) {
0796: return getTargetPage(request, currentPage);
0797: }
0798:
0799: /**
0800: * Return the target page specified in the request.
0801: * <p>Default implementation examines "_target" parameter (e.g. "_target1").
0802: * Subclasses can override this for customized target page determination.
0803: * @param request current portlet request
0804: * @param currentPage the current page, to be returned as fallback
0805: * if no target page specified
0806: * @return the page specified in the request, or current page if not found
0807: * @see #PARAM_TARGET
0808: */
0809: protected int getTargetPage(PortletRequest request, int currentPage) {
0810: Enumeration paramNames = request.getParameterNames();
0811: while (paramNames.hasMoreElements()) {
0812: String paramName = (String) paramNames.nextElement();
0813: if (paramName.startsWith(PARAM_TARGET)) {
0814: for (int i = 0; i < WebUtils.SUBMIT_IMAGE_SUFFIXES.length; i++) {
0815: String suffix = WebUtils.SUBMIT_IMAGE_SUFFIXES[i];
0816: if (paramName.endsWith(suffix)) {
0817: paramName = paramName.substring(0, paramName
0818: .length()
0819: - suffix.length());
0820: }
0821: }
0822: return Integer.parseInt(paramName
0823: .substring(PARAM_TARGET.length()));
0824: }
0825: }
0826: return currentPage;
0827: }
0828:
0829: /**
0830: * Validate all pages and process finish.
0831: * If there are page validation errors, show the corresponding view page.
0832: * @see #validatePagesAndFinish
0833: */
0834: private ModelAndView renderValidatePagesAndFinish(
0835: RenderRequest request, RenderResponse response,
0836: Object command, BindException errors, int currentPage)
0837: throws Exception {
0838:
0839: // In case of any errors -> show current page.
0840: if (errors.hasErrors())
0841: return showPage(request, errors, currentPage);
0842:
0843: // No remaining errors -> proceed with finish.
0844: return renderFinish(request, response, command, errors);
0845: }
0846:
0847: /**
0848: * Validate all pages and process finish.
0849: * If there are page validation errors, show the corresponding view page.
0850: * @see #renderValidatePagesAndFinish
0851: */
0852: private void validatePagesAndFinish(ActionRequest request,
0853: ActionResponse response, Object command,
0854: BindException errors, int currentPage) throws Exception {
0855:
0856: // In case of binding errors -> show current page.
0857: if (errors.hasErrors()) {
0858: setPageRenderParameter(response, currentPage);
0859: passRenderParameters(request, response);
0860: return;
0861: }
0862:
0863: if (!suppressValidation(request)) {
0864: // In case of remaining errors on a page -> show the page.
0865: for (int page = 0; page < getPageCount(request, command); page++) {
0866: validatePage(command, errors, page, true);
0867: if (errors.hasErrors()) {
0868: setPageRenderParameter(response, currentPage);
0869: passRenderParameters(request, response);
0870: return;
0871: }
0872: }
0873: }
0874:
0875: // No remaining errors -> proceed with finish.
0876: if (!isRedirectAction())
0877: setPageRenderParameter(response, currentPage);
0878: processFinish(request, response, command, errors);
0879:
0880: }
0881:
0882: /**
0883: * Template method for custom validation logic for individual pages.
0884: * Default implementation calls <code>validatePage(command, errors, page)</code>.
0885: * <p>Implementations will typically call fine-granular <code>validateXXX</code>
0886: * methods of this instance's Validator, combining them to validation of the
0887: * corresponding pages. The Validator's default <code>validate</code> method
0888: * will not be called by a wizard form controller!
0889: * @param command form object with the current wizard state
0890: * @param errors validation errors holder
0891: * @param page number of page to validate
0892: * @param finish whether this method is called during final revalidation on finish
0893: * (else, it is called for validating the current page)
0894: * @see #validatePage(Object, Errors, int)
0895: * @see org.springframework.validation.Validator#validate
0896: */
0897: protected void validatePage(Object command, Errors errors,
0898: int page, boolean finish) {
0899: validatePage(command, errors, page);
0900: }
0901:
0902: /**
0903: * Template method for custom validation logic for individual pages.
0904: * Default implementation is empty.
0905: * <p>Implementations will typically call fine-granular validateXXX methods of this
0906: * instance's validator, combining them to validation of the corresponding pages.
0907: * The validator's default <code>validate</code> method will not be called by a
0908: * wizard form controller!
0909: * @param command form object with the current wizard state
0910: * @param errors validation errors holder
0911: * @param page number of page to validate
0912: * @see org.springframework.validation.Validator#validate
0913: */
0914: protected void validatePage(Object command, Errors errors, int page) {
0915: }
0916:
0917: /**
0918: * Post-process the given page after binding and validation, potentially
0919: * updating its command object. The passed-in request might contain special
0920: * parameters sent by the page.
0921: * <p>Only invoked when displaying another page or the same page again,
0922: * not when finishing or cancelling.
0923: * @param request current action request
0924: * @param command form object with request parameters bound onto it
0925: * @param errors validation errors holder
0926: * @param page number of page to post-process
0927: * @throws Exception in case of invalid state or arguments
0928: */
0929: protected void postProcessPage(ActionRequest request,
0930: Object command, Errors errors, int page) throws Exception {
0931: }
0932:
0933: /**
0934: * Template method for the render phase of the finish action of this wizard.
0935: * <p>Default implementation throws a PortletException, saying that a finish
0936: * render request is not supported by this controller. Thus, you do not need to
0937: * implement this template method if you do not need to render after a finish.
0938: * <p>Call <code>errors.getModel()</code> to populate the ModelAndView model
0939: * with the command and the Errors instance, under the specified command name,
0940: * as expected by the "spring:bind" tag.
0941: * @param request current portlet render request
0942: * @param response current portlet render response
0943: * @param command form object with the current wizard state
0944: * @param errors validation errors holder
0945: * @return the finish view
0946: * @throws Exception in case of invalid state or arguments
0947: * @see #processFinish
0948: * @see org.springframework.validation.Errors
0949: * @see org.springframework.validation.BindException#getModel
0950: */
0951: protected ModelAndView renderFinish(RenderRequest request,
0952: RenderResponse response, Object command,
0953: BindException errors) throws Exception {
0954:
0955: throw new PortletException("Wizard form controller class ["
0956: + getClass().getName()
0957: + "] does not support a finish render request");
0958: }
0959:
0960: /**
0961: * Template method for the action phase of the finish action of this wizard.
0962: * <p>Default implementation throws a PortletException, saying that a finish
0963: * action request is not supported by this controller. You will almost certainly
0964: * need to override this method.
0965: * @param request current portlet action request
0966: * @param response current portlet action response
0967: * @param command form object with the current wizard state
0968: * @param errors validation errors holder
0969: * @throws Exception in case of invalid state or arguments
0970: * @see #renderFinish
0971: * @see org.springframework.validation.Errors
0972: */
0973: protected void processFinish(ActionRequest request,
0974: ActionResponse response, Object command,
0975: BindException errors) throws Exception {
0976:
0977: throw new PortletException("Wizard form controller class ["
0978: + getClass().getName()
0979: + "] does not support a finish action request");
0980: }
0981:
0982: /**
0983: * Template method for the render phase of the cancel action of this wizard.
0984: * <p>Default implementation throws a PortletException, saying that a cancel
0985: * render request is not supported by this controller. Thus, you do not need to
0986: * implement this template method if you do not support a cancel operation.
0987: * <p>Call <code>errors.getModel()</code> to populate the ModelAndView model
0988: * with the command and the Errors instance, under the specified command name,
0989: * as expected by the "spring:bind" tag.
0990: * @param request current portlet render request
0991: * @param response current portlet render response
0992: * @param command form object with the current wizard state
0993: * @param errors Errors instance containing errors
0994: * @return the cancellation view
0995: * @throws Exception in case of invalid state or arguments
0996: * @see #processCancel
0997: * @see org.springframework.validation.Errors
0998: * @see org.springframework.validation.BindException#getModel
0999: */
1000: protected ModelAndView renderCancel(RenderRequest request,
1001: RenderResponse response, Object command,
1002: BindException errors) throws Exception {
1003:
1004: throw new PortletException("Wizard form controller class ["
1005: + getClass().getName()
1006: + "] does not support a cancel render request");
1007: }
1008:
1009: /**
1010: * Template method for the action phase of the cancel action of this wizard.
1011: * <p>Default implementation throws a PortletException, saying that a cancel
1012: * action request is not supported by this controller. Thus, you do not need to
1013: * implement this template method if you do not support a cancel operation.
1014: * @param request current portlet action request
1015: * @param response current portlet action response
1016: * @param command form object with the current wizard state
1017: * @param errors Errors instance containing errors
1018: * @throws Exception in case of invalid state or arguments
1019: * @see #renderCancel
1020: * @see org.springframework.validation.Errors
1021: */
1022: protected void processCancel(ActionRequest request,
1023: ActionResponse response, Object command,
1024: BindException errors) throws Exception {
1025:
1026: throw new PortletException("Wizard form controller class ["
1027: + getClass().getName()
1028: + "] does not support a cancel action request");
1029: }
1030:
1031: }
|