/* * Copyright 2004-2012 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.webflow.action; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.PropertyEditorRegistrar; import org.springframework.beans.PropertyEditorRegistry; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.propertyeditors.PropertiesEditor; import org.springframework.core.style.StylerUtils; import org.springframework.core.style.ToStringCreator; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.DataBinder; import org.springframework.validation.Errors; import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; import org.springframework.web.bind.WebDataBinder; import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; import org.springframework.webflow.execution.ScopeType; /** * Multi-action that implements common logic dealing with input forms. This class uses the Spring Web data binding code * to do binding and validation. * <p> * Several action execution methods are provided: * <ul> * <li> {@link #setupForm(RequestContext)} - Prepares the form object for display on a form, * {@link #createFormObject(RequestContext) creating it} and an associated {@link Errors errors} if necessary, then * caching them in the configured {@link #getFormObjectScope() form object scope} and {@link #getFormErrorsScope() form * errors scope}, respectively. Also {@link #registerPropertyEditors(PropertyEditorRegistry) installs} any custom * property editors for formatting form object field values. This action method will return the "success" event unless * an exception is thrown.</li> * <li> {@link #bindAndValidate(RequestContext)} - Binds all incoming request parameters to the form object and then * validates the form object using a {@link #setValidator(Validator) registered validator}. This action method will * return the "success" event if there are no binding or validation errors, otherwise it will return the "error" event.</li> * <li> {@link #bind(RequestContext)} - Binds all incoming request parameters to the form object. No additional * validation is performed. This action method will return the "success" event if there are no binding errors, otherwise * it will return the "error" event.</li> * <li> {@link #validate(RequestContext)} - Validates the form object using a registered validator. No data binding is * performed. This action method will return the "success" event if there are no validation errors, otherwise it will * return the "error" event.</li> * <li> {@link #resetForm(RequestContext)} - Resets the form by reloading the backing form object and reinstalling any * custom property editors. Returns "success" on completion, an exception is thrown when a failure occurs.</li> * </ul> * <p> * Since this is a multi-action a subclass could add any number of additional action execution methods, e.g. * "setupReferenceData(RequestContext)", or "processSubmit(RequestContext)". * <p> * Using this action, it becomes very easy to implement form preparation and submission logic in your flow. One way to * do this follows: * <ol> * <li>Create a view state to display the form. In a render action of that state, invoke * {@link #setupForm(RequestContext) setupForm} to prepare the new form for display.</li> * <li>On a matching "submit" transition execute an action that invokes {@link #bindAndValidate(RequestContext) * bindAndValidate} to bind incoming request parameters to the form object and validate the form object.</li> * <li>If there are binding or validation errors, the transition will not be allowed and the view state will * automatically be re-entered. * <li>If binding and validation are successful go to an action state called "processSubmit" (or any other appropriate * name). This will invoke an action method called "processSubmit" you must provide on a subclass to process form * submission, e.g. interacting with the business logic.</li> * <li>If business processing is ok, continue to a view state to display the success view.</li> * </ol> * <p> * Here is an example implementation of such a form flow: * * <pre> * <view-state id="displayCriteria"> * <on-render> * <evaluate expression="formAction.setupForm"/> * </on-render> * <transition on="search" to="executeSearch"> * <evaluate expression="formAction.bindAndValidate"/> * </transition> * </view-state> * </pre> * * <p> * When you need additional flexibility consider splitting the view state above acting as a single logical <i>form * state</i> into multiple states. For example, you could have one action state handle form setup, a view state trigger * form display, another action state handle data binding and validation, and another process form submission. This * would be a bit more verbose but would also give you more control over how you respond to specific results of * fine-grained actions that occur within the flow. * <p> * <b>Subclassing hooks:</b> * <ul> * <li>A important hook is {@link #createFormObject(RequestContext) createFormObject}. You may override this to * customize where the backing form object instance comes from (e.g instantiated transiently in memory or loaded from a * database).</li> * <li>An optional hook method provided by this class is {@link #initBinder(RequestContext, DataBinder) initBinder}. * This is called after a new data binder is created. * <li>Another optional hook method is {@link #registerPropertyEditors(PropertyEditorRegistry)}. By overriding it you * can register any required property editors for your form. Instead of overriding this method, consider setting an * explicit {@link org.springframework.beans.PropertyEditorRegistrar} strategy as a more reusable way to encapsulate * custom PropertyEditor installation logic.</li> * <li>Override {@link #validationEnabled(RequestContext)} to dynamically decide whether or not to do validation based * on data available in the request context. * </ul> * <p> * Note that this action does not provide a <i>referenceData()</i> hook method similar to that of Spring MVC's * <code>SimpleFormController</code>. If you wish to expose reference data to populate form drop downs you can define a * custom action method in your FormAction subclass that does just that. Simply invoke it as either a chained action as * part of the setupForm state, or as a fine grained state definition itself. * <p> * For example, you might create this method in your subclass: * * <pre> * public Event setupReferenceData(RequestContext context) throws Exception { * MutableAttributeMap requestScope = context.getRequestScope(); * requestScope.put("refData", lookupService.getSupportingFormData()); * return success(); * } * </pre> * * ... and then invoke it like this: * * <pre> * <view-state id="displayCriteria"> * <on-render> * <evaluate expression="formAction.setupForm"/> * <evaluate expression="formAction.setupReferenceData"/> * </on-render> * ... * </view-state> * </pre> * * This style of calling multiple action methods in a chain (Chain of Responsibility) is preferred to overriding a * single action method. In general, action method overriding is discouraged. * <p> * When it comes to validating submitted input data using a registered {@link org.springframework.validation.Validator}, * this class offers the following options: * <ul> * <li>If you don't want validation at all, just call {@link #bind(RequestContext)} instead of * {@link #bindAndValidate(RequestContext)} or don't register a validator.</li> * <li>If you want piecemeal validation, e.g. in a multi-page wizard, call {@link #bindAndValidate(RequestContext)} or * {@link #validate(RequestContext)} and specify a {@link #VALIDATOR_METHOD_ATTRIBUTE validatorMethod} action execution * attribute. This will invoke the identified custom validator method on the validator. The validator method signature * should follow the following pattern: * * <pre> * public void ${validateMethodName}(${formObjectClass}, Errors) * </pre> * * For instance, having a action definition like this: * * <pre> * <evaluate expression="formAction.bindAndValidate"> * <attribute name="validatorMethod" value="validateSearchCriteria"/> * </evaluate> * </pre> * * Would result in the <tt>public void validateSearchCriteria(SearchCriteria, Errors)</tt> method of the registered * validator being called if the form object class would be <code>SearchCriteria</code>.</li> * <li>If you want to do full validation using the * {@link org.springframework.validation.Validator#validate(java.lang.Object, org.springframework.validation.Errors) * validate} method of the registered validator, call {@link #bindAndValidate(RequestContext)} or * {@link #validate(RequestContext)} without specifying a "validatorMethod" action execution attribute.</li> * </ul> * * <p> * <b>FormAction configurable properties</b><br> * <table border="1"> * <tr> * <td><b>name</b></td> * <td><b>default</b></td> * <td><b>description</b></td> * </tr> * <tr> * <td>formObjectName</td> * <td>formObject</td> * <td>The name of the form object. The form object will be set in the configured scope using this name.</td> * </tr> * <tr> * <td>formObjectClass</td> * <td>null</td> * <td>The form object class for this action. An instance of this class will get populated and validated. Required when * using a validator.</td> * </tr> * <tr> * <td>formObjectScope</td> * <td>{@link org.springframework.webflow.execution.ScopeType#FLOW flow}</td> * <td>The scope in which the form object will be put. If put in flow scope the object will be cached and reused over * the life of the flow, preserving previous values. Request scope will cause a new fresh form object instance to be * created on each request into the flow execution.</td> * </tr> * <tr> * <td>formErrorsScope</td> * <td>{@link org.springframework.webflow.execution.ScopeType#FLASH flash}</td> * <td>The scope in which the form object errors instance will be put. If put in flash scope form errors will be cached * until the next user event is signaled.</td> * </tr> * <tr> * <td>propertyEditorRegistrar</td> * <td>null</td> * <td>The strategy used to register custom property editors with the data binder. This is an alternative to overriding * the {@link #registerPropertyEditors(PropertyEditorRegistry)} hook method.</td> * </tr> * <tr> * <td>validator</td> * <td>null</td> * <td>The validator for this action. The validator must support the specified form object class.</td> * </tr> * <tr> * <td>messageCodesResolver</td> * <td>null</td> * <td>Set the strategy to use for resolving errors into message codes.</td> * </tr> * </table> * * @see org.springframework.beans.PropertyEditorRegistrar * @see org.springframework.validation.DataBinder * @see ScopeType * * @author Erwin Vervaet * @author Keith Donald */ public class FormAction extends MultiAction implements InitializingBean { /** * The default form object name ("formObject"). */ public static final String DEFAULT_FORM_OBJECT_NAME = "formObject"; /** * Optional attribute that identifies the method that should be invoked on the configured validator instance, to * support piecemeal wizard page validation ("validatorMethod"). */ public static final String VALIDATOR_METHOD_ATTRIBUTE = "validatorMethod"; /** * The name the form object should be exposed under. Default is {@link #DEFAULT_FORM_OBJECT_NAME}. */ private String formObjectName = DEFAULT_FORM_OBJECT_NAME; /** * The type of form object, typically an instantiable class. Required if {@link #createFormObject(RequestContext)} * is not overridden or when a validator is used. */ private Class<?> formObjectClass; /** * The scope in which the form object should be exposed. Default is {@link ScopeType#FLOW}. */ private ScopeType formObjectScope = ScopeType.FLOW; /** * The scope in which the form object errors holder should be exposed. Default is {@link ScopeType#FLASH}. */ private ScopeType formErrorsScope = ScopeType.FLASH; /** * A centralized service for property editor registration, for applying type conversion during form object data * binding. Can be used as an alternative to overriding {@link #registerPropertyEditors(PropertyEditorRegistry)}. */ private PropertyEditorRegistrar propertyEditorRegistrar; /** * A validator for the form's form object. */ private Validator validator; /** * Strategy for resolving error message codes. */ private MessageCodesResolver messageCodesResolver; /** * A cache for dispatched validator methods. */ private DispatchMethodInvoker validateMethodInvoker; /** * Bean-style default constructor; creates a initially unconfigured FormAction instance relying on default property * values. Clients invoking this constructor directly must set the formObjectClass property or override * {@link #createFormObject(RequestContext)}. * @see #setFormObjectClass(Class) */ public FormAction() { } /** * Creates a new form action that manages instance(s) of the specified form object class. * @param formObjectClass the class of the form object (must be instantiable) */ public FormAction(Class<?> formObjectClass) { setFormObjectClass(formObjectClass); } /** * Return the name of the form object in the configured scope. */ public String getFormObjectName() { return formObjectName; } /** * Set the name of the form object in the configured scope. The form object will be included in the configured scope * under this name. */ public void setFormObjectName(String formObjectName) { this.formObjectName = formObjectName; } /** * Return the form object class for this action. */ public Class<?> getFormObjectClass() { return formObjectClass; } /** * Set the form object class for this action. An instance of this class will get populated and validated. This is a * required property if you register a validator with the form action ({@link #setValidator(Validator)})! * <p> * If no form object name is set at the moment this method is called, a form object name will be automatically * generated based on the provided form object class using * {@link ClassUtils#getShortNameAsProperty(java.lang.Class)}. */ public void setFormObjectClass(Class<?> formObjectClass) { this.formObjectClass = formObjectClass; // generate a default form object name if ((getFormObjectName() == null || getFormObjectName() == DEFAULT_FORM_OBJECT_NAME) && formObjectClass != null) { setFormObjectName(ClassUtils.getShortNameAsProperty(formObjectClass)); } } /** * Get the scope in which the form object will be placed. */ public ScopeType getFormObjectScope() { return formObjectScope; } /** * Set the scope in which the form object will be placed. The default if not set is {@link ScopeType#FLOW flow * scope}. */ public void setFormObjectScope(ScopeType scopeType) { this.formObjectScope = scopeType; } /** * Get the scope in which the Errors object will be placed. */ public ScopeType getFormErrorsScope() { return formErrorsScope; } /** * Set the scope in which the Errors object will be placed. The default if not set is {@link ScopeType#FLASH flash * scope}. */ public void setFormErrorsScope(ScopeType errorsScope) { this.formErrorsScope = errorsScope; } /** * Get the property editor registration strategy for this action's data binders. */ public PropertyEditorRegistrar getPropertyEditorRegistrar() { return propertyEditorRegistrar; } /** * Set a property editor registration strategy for this action's data binders. This is an alternative to overriding * the {@link #registerPropertyEditors(PropertyEditorRegistry)} method. */ public void setPropertyEditorRegistrar(PropertyEditorRegistrar propertyEditorRegistrar) { this.propertyEditorRegistrar = propertyEditorRegistrar; } /** * Returns the validator for this action. */ public Validator getValidator() { return validator; } /** * Set the validator for this action. When setting a validator, you must also set the * {@link #setFormObjectClass(Class) form object class}. The validator must support the specified form object class. */ public void setValidator(Validator validator) { this.validator = validator; } /** * Return the strategy to use for resolving errors into message codes. */ public MessageCodesResolver getMessageCodesResolver() { return messageCodesResolver; } /** * Set the strategy to use for resolving errors into message codes. Applies the given strategy to all data binders * used by this action. * <p> * Default is null, i.e. using the default strategy of the data binder. * @see #createBinder(RequestContext, Object) * @see org.springframework.validation.DataBinder#setMessageCodesResolver(org.springframework.validation.MessageCodesResolver) */ public void setMessageCodesResolver(MessageCodesResolver messageCodesResolver) { this.messageCodesResolver = messageCodesResolver; } protected void initAction() { if (getValidator() != null) { if (getFormObjectClass() != null && !getValidator().supports(getFormObjectClass())) { throw new IllegalArgumentException("Validator [" + getValidator() + "] does not support form object class [" + getFormObjectClass() + "]"); } // signature: public void ${validateMethodName}(${formObjectClass}, Errors) validateMethodInvoker = new DispatchMethodInvoker(getValidator(), new Class[] { getFormObjectClass(), Errors.class }); } } // action methods /** * Prepares a form object for display in a new form, creating it and caching it in the {@link #getFormObjectScope()} * if necessary. Also installs custom property editors for formatting form object values in UI controls such as text * fields. * <p> * A new form object instance will only be created (or more generally acquired) with a call to * {@link #createFormObject(RequestContext)}, if the form object does not yet exist in the configured * {@link #getFormObjectScope() scope}. If you want to reset the form handling machinery, including creation or * loading of a fresh form object instance, call {@link #resetForm(RequestContext)} instead. * <p> * NOTE: This action method is not designed to be overridden and might become <code>final</code> in a future version * of Spring Web Flow. If you need to execute custom form setup logic have your flow call this method along with * your own custom methods as part of a single action chain. * @param context the action execution context, for accessing and setting data in "flow scope" or "request scope" * @return "success" when binding and validation is successful * @throws Exception an <b>unrecoverable</b> exception occurs, either checked or unchecked * @see #createFormObject(RequestContext) */ public Event setupForm(RequestContext context) throws Exception { if (logger.isDebugEnabled()) { logger.debug("Executing setupForm"); } // retrieve the form object, creating it if necessary Object formObject = getFormObject(context); ensureFormErrorsExposed(context, formObject); return success(); } /** * Bind incoming request parameters to allowed fields of the form object and then validate the bound form object if * a validator is configured. * <p> * NOTE: This action method is not designed to be overridden and might become <code>final</code> in a future version * of Spring Web Flow. If you need to execute custom bind and validate logic have your flow call this method along * with your own custom methods as part of a single action chain. Alternatively, override the * {@link #doBind(RequestContext, DataBinder)} or {@link #doValidate(RequestContext, Object, Errors)} hooks. * @param context the action execution context, for accessing and setting data in "flow scope" or "request scope" * @return "success" when binding and validation is successful, "error" if there were binding and/or validation * errors * @throws Exception an <b>unrecoverable</b> exception occurred, either checked or unchecked */ public Event bindAndValidate(RequestContext context) throws Exception { if (logger.isDebugEnabled()) { logger.debug("Executing bind"); } Object formObject = getFormObject(context); DataBinder binder = createBinder(context, formObject); doBind(context, binder); if (getValidator() != null && validationEnabled(context)) { if (logger.isDebugEnabled()) { logger.debug("Executing validation"); } doValidate(context, formObject, binder.getBindingResult()); } else { if (logger.isDebugEnabled()) { if (getValidator() == null) { logger.debug("No validator is configured, no validation will occur after binding"); } else { logger.debug("Validation was disabled for this bindAndValidate request"); } } } putFormErrors(context, binder.getBindingResult()); return binder.getBindingResult().hasErrors() ? error() : success(); } /** * Bind incoming request parameters to allowed fields of the form object. * <p> * NOTE: This action method is not designed to be overridden and might become <code>final</code> in a future version * of Spring Web Flow. If you need to execute custom data binding logic have your flow call this method along with * your own custom methods as part of a single action chain. Alternatively, override the * {@link #doBind(RequestContext, DataBinder)} hook. * @param context the action execution context, for accessing and setting data in "flow scope" or "request scope" * @return "success" if there are no binding errors, "error" otherwise * @throws Exception an <b>unrecoverable</b> exception occured, either checked or unchecked */ public Event bind(RequestContext context) throws Exception { if (logger.isDebugEnabled()) { logger.debug("Executing bind"); } Object formObject = getFormObject(context); DataBinder binder = createBinder(context, formObject); doBind(context, binder); putFormErrors(context, binder.getBindingResult()); return binder.getBindingResult().hasErrors() ? error() : success(); } /** * Validate the form object by invoking the validator if configured. * <p> * NOTE: This action method is not designed to be overridden and might become <code>final</code> in a future version * of Spring Web Flow. If you need to execute custom validation logic have your flow call this method along with * your own custom methods as part of a single action chain. Alternatively, override the * {@link #doValidate(RequestContext, Object, Errors)} hook. * @param context the action execution context, for accessing and setting data in "flow scope" or "request scope" * @return "success" if there are no validation errors, "error" otherwise * @throws Exception an <b>unrecoverable</b> exception occured, either checked or unchecked * @see #getValidator() */ public Event validate(RequestContext context) throws Exception { if (getValidator() != null && validationEnabled(context)) { if (logger.isDebugEnabled()) { logger.debug("Executing validation"); } Object formObject = getFormObject(context); Errors errors = getFormErrors(context); doValidate(context, formObject, errors); return errors.hasErrors() ? error() : success(); } else { if (logger.isDebugEnabled()) { if (getValidator() == null) { logger.debug("No validator is configured, no validation will occur"); } else { logger.debug("Validation was disabled for this request"); } } return success(); } } /** * Resets the form by clearing out the form object in the specified scope and recreating it. * <p> * NOTE: This action method is not designed to be overridden and might become <code>final</code> in a future version * of Spring Web Flow. If you need to execute custom reset logic have your flow call this method along with your own * custom methods as part of a single action chain. * @param context the request context * @return "success" if the reset action completed successfully * @throws Exception if an exception occured * @see #createFormObject(RequestContext) */ public Event resetForm(RequestContext context) throws Exception { Object formObject = initFormObject(context); initFormErrors(context, formObject); return success(); } // internal helpers /** * Create the new form object and put it in the configured {@link #getFormObjectScope() scope}. * @param context the flow execution request context * @return the new form object * @throws Exception an exception occured creating the form object */ private Object initFormObject(RequestContext context) throws Exception { if (logger.isDebugEnabled()) { logger.debug("Creating new form object with name '" + getFormObjectName() + "'"); } Object formObject = createFormObject(context); putFormObject(context, formObject); return formObject; } /** * Put given form object in the configured scope of given context. */ private void putFormObject(RequestContext context, Object formObject) { if (logger.isDebugEnabled()) { logger.debug("Putting form object of type [" + formObject.getClass() + "] in scope " + getFormObjectScope() + " with name '" + getFormObjectName() + "'"); } getFormObjectAccessor(context).putFormObject(formObject, getFormObjectName(), getFormObjectScope()); } /** * Initialize a new form object {@link Errors errors} instance in the configured {@link #getFormErrorsScope() scope} * . This method also registers any {@link PropertiesEditor property editors} used to format form object property * values. * @param context the current flow execution request context * @param formObject the form object for which errors will be tracked */ private Errors initFormErrors(RequestContext context, Object formObject) throws Exception { if (logger.isDebugEnabled()) { logger.debug("Creating new form errors for object with name '" + getFormObjectName() + "'"); } Errors errors = createBinder(context, formObject).getBindingResult(); putFormErrors(context, errors); return errors; } /** * Put given errors instance in the configured scope of given context. */ private void putFormErrors(RequestContext context, Errors errors) { if (logger.isDebugEnabled()) { logger.debug("Putting form errors instance in scope " + getFormErrorsScope()); } getFormObjectAccessor(context).putFormErrors(errors, getFormErrorsScope()); } /** * Make sure a <i>valid</i> Errors instance for given form object is exposed in given context. */ private void ensureFormErrorsExposed(RequestContext context, Object formObject) throws Exception { if (!formErrorsExposed(context)) { // initialize and expose a fresh errors instance to the flow with // editors applied initFormErrors(context, formObject); } else { // trying to reuse an existing errors instance if (formErrorsValid(context, formObject)) { // reapply property editors against the existing errors instance reinstallPropertyEditors(context); } else { // the existing errors instance seems to be invalid // initialize a new errors instance, but copy over error information if (logger.isInfoEnabled()) { logger.info("Fixing inconsistent Errors instance: initializing a new Errors instance " + "wrapping from object '" + formObject + "' in scope '" + getFormErrorsScope() + "' and copying over all existing error information."); } Errors invalidExistingErrors = getFormObjectAccessor(context).getFormErrors(getFormObjectName(), getFormErrorsScope()); Errors newErrors = initFormErrors(context, formObject); newErrors.addAllErrors(invalidExistingErrors); } } } /** * Check if there is an Errors instance available in given context for given form object. */ private boolean formErrorsExposed(RequestContext context) { return getFormObjectAccessor(context).getFormErrors(getFormObjectName(), getFormErrorsScope()) != null; } /** * Check if the Errors instance available in given context is valid for given form object. */ private boolean formErrorsValid(RequestContext context, Object formObject) { Errors errors = getFormObjectAccessor(context).getFormErrors(getFormObjectName(), getFormErrorsScope()); if (errors instanceof BindingResult) { BindingResult be = (BindingResult) errors; if (be.getTarget() != formObject) { if (logger.isInfoEnabled()) { logger.info("Inconsistency detected: the Errors instance in '" + getFormErrorsScope() + "' does NOT wrap the current form object '" + formObject + "' of class " + formObject.getClass() + "; instead this Errors instance unexpectedly wraps the target object '" + be.getTarget() + "' of class: " + be.getTarget().getClass() + "."); } return false; } else { return true; } } else { return true; } } /** * Re-registers property editors against the current form errors instance. * @param context the flow execution request context */ private void reinstallPropertyEditors(RequestContext context) { BindingResult errors = (BindingResult) getFormObjectAccessor(context).getFormErrors(getFormObjectName(), getFormErrorsScope()); registerPropertyEditors(context, errors.getPropertyEditorRegistry()); } /** * Invoke specified validator method on the validator registered with this action. The validator method for * piecemeal validation should have the following signature: * * <pre> * public void ${validateMethodName}(${formObjectClass}, Errors) * </pre> * * @param validatorMethod the name of the validator method to invoke * @param formObject the form object * @param errors possible binding errors * @throws Exception when an unrecoverable exception occurs */ private void invokeValidatorMethod(String validatorMethod, Object formObject, Errors errors) throws Exception { if (logger.isDebugEnabled()) { logger.debug("Invoking piecemeal validator method '" + validatorMethod + "(" + getFormObjectClass() + ", Errors)'"); } getValidateMethodInvoker().invoke(validatorMethod, new Object[] { formObject, errors }); } // accessible helpers (subclasses could override if necessary) /** * Convenience method that returns the form object for this form action. If not found in the configured scope, a new * form object will be created by a call to {@link #createFormObject(RequestContext)} and exposed in the configured * {@link #getFormObjectScope() scope}. * <p> * The returned form object will become the {@link FormObjectAccessor#setCurrentFormObject(Object, ScopeType) * current} form object. * @param context the flow execution request context * @return the form object * @throws Exception when an unrecoverable exception occurs */ protected Object getFormObject(RequestContext context) throws Exception { FormObjectAccessor accessor = getFormObjectAccessor(context); Object formObject = accessor.getFormObject(getFormObjectName(), getFormObjectScope()); if (formObject == null) { formObject = initFormObject(context); } else { if (logger.isDebugEnabled()) { logger.debug("Found existing form object with name '" + getFormObjectName() + "' of type [" + formObject.getClass() + "] in scope " + getFormObjectScope()); } accessor.setCurrentFormObject(formObject, getFormObjectScope()); } return formObject; } /** * Convenience method that returns the form object errors for this form action. If not found in the configured * scope, a new form object errors will be created, initialized, and exposed in the confgured * {@link #getFormErrorsScope() scope}. * <p> * Keep in mind that an Errors instance wraps a form object, so a form object will also be created if required (see * {@link #getFormObject(RequestContext)}). * @param context the flow request context * @return the form errors * @throws Exception when an unrecoverable exception occurs */ protected Errors getFormErrors(RequestContext context) throws Exception { Object formObject = getFormObject(context); ensureFormErrorsExposed(context, formObject); return getFormObjectAccessor(context).getFormErrors(getFormObjectName(), getFormErrorsScope()); } /** * Create a new binder instance for the given form object and request context. Can be overridden to plug in custom * DataBinder subclasses. * <p> * Default implementation creates a standard WebDataBinder, and invokes * {@link #initBinder(RequestContext, DataBinder)} and {@link #registerPropertyEditors(PropertyEditorRegistry)}. * @param context the action execution context, for accessing and setting data in "flow scope" or "request scope" * @param formObject the form object to bind onto * @return the new binder instance * @throws Exception when an unrecoverable exception occurs * @see WebDataBinder * @see #initBinder(RequestContext, DataBinder) * @see #setMessageCodesResolver(MessageCodesResolver) */ protected DataBinder createBinder(RequestContext context, Object formObject) throws Exception { DataBinder binder = new WebDataBinder(formObject, getFormObjectName()); if (getMessageCodesResolver() != null) { binder.setMessageCodesResolver(getMessageCodesResolver()); } initBinder(context, binder); registerPropertyEditors(context, binder); return binder; } /** * Bind allowed parameters in the external context request parameter map to the form object using given binder. * @param context the action execution context, for accessing and setting data in "flow scope" or "request scope" * @param binder the data binder to use * @throws Exception when an unrecoverable exception occurs */ protected void doBind(RequestContext context, DataBinder binder) throws Exception { if (logger.isDebugEnabled()) { logger.debug("Binding allowed request parameters in " + StylerUtils.style(context.getExternalContext().getRequestParameterMap()) + " to form object with name '" + binder.getObjectName() + "', pre-bind formObject toString = " + binder.getTarget()); if (binder.getAllowedFields() != null && binder.getAllowedFields().length > 0) { logger.debug("(Allowed fields are " + StylerUtils.style(binder.getAllowedFields()) + ")"); } else { logger.debug("(Any field is allowed)"); } } binder.bind(new MutablePropertyValues(context.getRequestParameters().asMap())); if (logger.isDebugEnabled()) { logger.debug("Binding completed for form object with name '" + binder.getObjectName() + "', post-bind formObject toString = " + binder.getTarget()); logger.debug("There are [" + binder.getBindingResult().getErrorCount() + "] errors, details: " + binder.getBindingResult().getAllErrors()); } } /** * Validate given form object using a registered validator. If a "validatorMethod" action property is specified for * the currently executing action, the identified validator method will be invoked. When no such property is found, * the defualt <code>validate()</code> method is invoked. * @param context the action execution context, for accessing and setting data in "flow scope" or "request scope" * @param formObject the form object * @param errors the errors instance to record validation errors in * @throws Exception when an unrecoverable exception occurs */ protected void doValidate(RequestContext context, Object formObject, Errors errors) throws Exception { Assert.notNull(getValidator(), "The validator must not be null when attempting validation -- programmer error"); String validatorMethodName = context.getAttributes().getString(VALIDATOR_METHOD_ATTRIBUTE); if (StringUtils.hasText(validatorMethodName)) { if (logger.isDebugEnabled()) { logger.debug("Invoking validation method '" + validatorMethodName + "' on validator " + getValidator()); } invokeValidatorMethod(validatorMethodName, formObject, errors); } else { if (logger.isDebugEnabled()) { logger.debug("Invoking validator " + getValidator()); } getValidator().validate(formObject, errors); } if (logger.isDebugEnabled()) { logger.debug("Validation completed for form object"); logger.debug("There are [" + errors.getErrorCount() + "] errors, details: " + errors.getAllErrors()); } } /** * Returns a dispatcher to invoke validation methods. Subclasses could override this to return a custom dispatcher. */ protected DispatchMethodInvoker getValidateMethodInvoker() { return validateMethodInvoker; } /** * Factory method that returns a new form object accessor for accessing form objects in the provided request * context. * @param context the flow request context * @return the accessor */ protected FormObjectAccessor getFormObjectAccessor(RequestContext context) { return new FormObjectAccessor(context); } // common subclassing hook methods /** * Create the backing form object instance that should be managed by this {@link FormAction form action}. By * default, will attempt to instantiate a new form object instance of type {@link #getFormObjectClass()} transiently * in memory. * <p> * Subclasses should override if they need to load the form object from a specific location or resource such as a * database or filesystem. * <p> * Subclasses should override if they need to customize how a transient form object is assembled during creation. * @param context the action execution context for accessing flow data * @return the form object * @throws IllegalStateException if the {@link #getFormObjectClass()} property is not set and this method has not * been overridden * @throws Exception when an unrecoverable exception occurs */ protected Object createFormObject(RequestContext context) throws Exception { if (getFormObjectClass() == null) { throw new IllegalStateException("Cannot create form object without formObjectClass property being set -- " + "either set formObjectClass or override createFormObject"); } if (logger.isDebugEnabled()) { logger.debug("Creating new instance of form object class [" + getFormObjectClass() + "]"); } return getFormObjectClass().newInstance(); } /** * Initialize a new binder instance. This hook allows customization of binder settings such as the * {@link DataBinder#getAllowedFields() allowed fields}, {@link DataBinder#getRequiredFields() required fields} and * {@link DataBinder#initDirectFieldAccess() direct field access}. Called by * {@link #createBinder(RequestContext, Object)}. * <p> * Note that registration of custom property editors should be done in * {@link #registerPropertyEditors(PropertyEditorRegistry)}, not here! This method will only be called when a * <b>new</b> data binder is created. * @param context the action execution context, for accessing and setting data in "flow scope" or "request scope" * @param binder new binder instance * @see #createBinder(RequestContext, Object) */ protected void initBinder(RequestContext context, DataBinder binder) { } /** * Register custom editors to perform type conversion on fields of your form object during data binding and form * display. This method is called on form errors initialization and {@link #initBinder(RequestContext, DataBinder) * data binder} initialization. * <p> * Property editors give you full control over how objects are transformed to and from a formatted String form for * display on a user interface such as a HTML page. * <p> * This default implementation will call the {@link #registerPropertyEditors(PropertyEditorRegistry) simpler form} * of the method not taking a <tt>RequestContext</tt> parameter. * @param context the action execution context, for accessing and setting data in "flow scope" or "request scope" * @param registry the property editor registry to register editors in * @see #registerPropertyEditors(PropertyEditorRegistry) */ protected void registerPropertyEditors(RequestContext context, PropertyEditorRegistry registry) { registerPropertyEditors(registry); } /** * Register custom editors to perform type conversion on fields of your form object during data binding and form * display. This method is called on form errors initialization and {@link #initBinder(RequestContext, DataBinder) * data binder} initialization. * <p> * Property editors give you full control over how objects are transformed to and from a formatted String form for * display on a user interface such as a HTML page. * <p> * This default implementation will simply call <tt>registerCustomEditors</tt> on the * {@link #getPropertyEditorRegistrar() propertyEditorRegistrar} object that has been set for the action, if any. * @param registry the property editor registry to register editors in */ protected void registerPropertyEditors(PropertyEditorRegistry registry) { if (getPropertyEditorRegistrar() != null) { if (logger.isDebugEnabled()) { logger.debug("Registering custom property editors using configured registrar"); } getPropertyEditorRegistrar().registerCustomEditors(registry); } else { if (logger.isDebugEnabled()) { logger.debug("No property editor registrar set, no custom editors to register"); } } } /** * Return whether validation should be performed given the state of the flow request context. Default implementation * always returns true. * @param context the request context, for accessing and setting data in "flow scope" or "request scope" * @return whether or not validation is enabled */ protected boolean validationEnabled(RequestContext context) { return true; } public String toString() { return new ToStringCreator(this).append("formObjectName", formObjectName) .append("formObjectClass", formObjectClass).append("formObjectScope", formObjectScope).toString(); } }