org.springframework.webflow.action.FormAction Maven / Gradle / Ivy
/*
* Copyright 2002-2006 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 java.lang.reflect.Method;
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.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindException;
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;
import org.springframework.webflow.util.DispatchMethodInvoker;
import org.springframework.webflow.util.ReflectionUtils;
/**
* Multi-action that implements common logic dealing with input forms. This
* class leverages the Spring Web data binding code to do binding and
* validation.
*
* Several action execution methods are provided:
*
* - {@link #setupForm(RequestContext)} - Prepares the form object for
* display on a form, {@link #createFormObject(RequestContext) creating it} and
* an associated {@link Errors errors object} 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.
* - {@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.
* - {@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.
* - {@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.
* - {@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.
*
*
* Since this is a multi-action a subclass could add any number of additional
* action execution methods, e.g. "setupReferenceData(RequestContext)", or
* "processSubmit(RequestContext)".
*
* Using this action, it becomes very easy to implement form preparation and
* submission logic in your flow. One way to do this follows:
*
* - 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.
* - 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.
* - If there are binding or validation errors, the transition will not be
* allowed and the view state will automatically be re-entered.
*
- If binding and validation is 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.
* - If business processing is ok, continue to a view state to display the
* success view.
*
*
* Here is an example implementation of such a compact form flow:
*
*
* <view-state id="displayCriteria" view="searchCriteria">
* <render-actions>
* <action bean="formAction" method="setupForm"/>
* </render-actions>
* <transition on="search" to="executeSearch">
* <action bean="formAction" method="bindAndValidate"/>
* </transition>
* </view-state>
*
* <action-state id="executeSearch">
* <action bean="formAction" method="executeSearch"/>
* <transition on="success" to="displayResults"/>
* </action-state>
*
*
*
* When you need additional flexibility consider splitting the view state above
* acting as a single logical form state 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.
*
* Subclassing hooks:
*
* - 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).
* - An optional hook method provided by this class is
* {@link #initBinder(RequestContext, DataBinder) initBinder}. This is called
* after a new data binder is created.
*
- Another optional ook 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.
* - Override {@link #validationEnabled(RequestContext)} to dynamically
* decide whether or not to do validation based on data available in the request
* context.
*
*
* Note that this action does not provide a referenceData() hook method
* similar to that of Spring MVC's SimpleFormController
. 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.
*
* For example, you might create this method in your subclass:
*
*
* public Event setupReferenceData(RequestContext context) throws Exception {
* MutableAttributeMap requestScope = context.getRequestScope();
* requestScope.put("refData", lookupService.getSupportingFormData());
* return success();
* }
*
*
* ... and then invoke it like this:
*
*
* <view-state id="displayCriteria" view="searchCriteria">
* <render-actions>
* <action bean="searchFormAction" method="setupForm"/>
* <action bean="searchFormAction" method="setupReferenceData"/>
* </render-actions>
* ...
* </view-state>
*
*
* This style of calling multiple action methods in a chain (Chain of
* Responsibility) is preferred to overridding a single action method. In
* general, action method overriding is discouraged.
*
* When it comes to validating submitted input data using a registered
* {@link org.springframework.validation.Validator}, this class offers the
* following options:
*
* - If you don't want validation at all, just call
* {@link #bind(RequestContext)} instead of
* {@link #bindAndValidate(RequestContext)} or don't register a validator.
* - 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:
*
*
* public void ${validateMethodName}(${formObjectClass}, Errors)
*
*
* For instance, having a action definition like this:
*
*
* <action bean="searchFormAction" method="bindAndValidate">
* <attribute name="validatorMethod" value="validateSearchCriteria"/>
* </action>
*
*
* Would result in the
* public void validateSearchCriteria(SearchCriteria, Errors) method
* of the registered validator being called if the form object class would be
* SearchCriteria
.
* - 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.
*
*
*
* FormAction configurable properties
*
*
* name
* default
* description
*
*
* formObjectName
* formObject
* The name of the form object. The form object will be set in the
* configured scope using this name.
*
*
* formObjectClass
* null
* The form object class for this action. An instance of this class will
* get populated and validated. Required when using a validator.
*
*
* formObjectScope
* {@link org.springframework.webflow.execution.ScopeType#FLOW flow}
* 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.
*
*
* formErrorsScope
* {@link org.springframework.webflow.execution.ScopeType#FLASH flash}
* 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.
*
*
*
* propertyEditorRegistrar
* null
* The strategy used to register custom property editors with the data
* binder. This is an alternative to overriding the
* {@link #registerPropertyEditors(PropertyEditorRegistry)} hook method.
*
*
* validator
* null
* The validator for this action. The validator must support the specified
* form object class.
*
*
* messageCodesResolver
* null
* Set the strategy to use for resolving errors into message codes.
*
*
*
* @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 {
/*
* Implementation note: Uses deprecated DataBinder.getErrors() to remain
* compatible with Spring 1.2.x.
*/
/*
* Implementation note: Introspects BindException at class init time to
* preserve 1.2.x compatability.
*/
private static boolean hasPropertyEditorRegistryAccessor;
static {
hasPropertyEditorRegistryAccessor = ClassUtils
.hasMethod(BindException.class, "getPropertyEditorRegistry", null);
}
/**
* 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 overidden 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 {@link #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)})!
*
* 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 ((formObjectName == null || formObjectName == DEFAULT_FORM_OBJECT_NAME) && formObjectClass != null) {
formObjectName = 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.
*
* 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) {
Assert.notNull(getFormObjectClass(), "When using a validator, the form object class is required");
if (!getValidator().supports(getFormObjectClass())) {
throw new IllegalArgumentException("Validator [" + getValidator()
+ "] does not support form object class [" + getFormObjectClass() + "]");
}
// signature: public void ${validateMethodName}(${formObjectClass}, Errors)
validateMethodInvoker = new DispatchMethodInvoker(validator, 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.
*
* NOTE: This is action method is not designed to be overidden and might
* become final
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 unrecoverable 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);
// ensure the form errors collection is created and exposed to the flow
if (!formErrorsExposed(context, formObject)) {
// initialize and expose a fresh errors instance to the flow with
// editors applied
initFormErrors(context, formObject);
}
else {
// reapply property editors against the existing errors instance
reinstallPropertyEditors(context);
}
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.
*
* NOTE: This action method is not designed to be overidden and might
* become final
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 unrecoverable exception occured, 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.getErrors());
}
else {
if (logger.isDebugEnabled()) {
if (validator == 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.getErrors());
return binder.getErrors().hasErrors() ? error() : success();
}
/**
* Bind incoming request parameters to allowed fields of the form object.
*
* NOTE: This is action method is not designed to be overidden and might
* become final
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 unrecoverable 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.getErrors());
return binder.getErrors().hasErrors() ? error() : success();
}
/**
* Validate the form object by invoking the validator if configured.
*
* NOTE: This is action method is not designed to be overidden and might
* become final
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 unrecoverable 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 (validator == 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.
*
* NOTE: This is action method is not designed to be overidden and might
* become final
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) {
if (logger.isDebugEnabled()) {
logger.debug("Creating new form errors for object with name '" + getFormObjectName() + "'");
}
Errors errors = createBinder(context, formObject).getErrors();
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());
}
/**
* Check if there is a valid Errors instance available in given
* context for given form object.
*/
private boolean formErrorsExposed(RequestContext context, Object formObject) {
Errors errors = getFormObjectAccessor(context).getFormErrors(getFormObjectName(), getFormErrorsScope());
if (errors instanceof BindException) {
// make sure the existing form errors are consistent with the form
// object
BindException be = (BindException)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; // a new Errors instance will be created
}
}
return errors != null;
}
/**
* Re-registers property editors against the current form errors instance.
* @param context the flow execution request context
*/
private void reinstallPropertyEditors(RequestContext context) {
BindException errors = (BindException)
getFormObjectAccessor(context).getFormErrors(getFormObjectName(), getFormErrorsScope());
registerPropertyEditors(context, getPropertyEditorRegistry(errors));
}
/**
* Obtain a property editor registry from given bind exception (errors
* instance).
*/
private PropertyEditorRegistry getPropertyEditorRegistry(BindException errors) {
Method accessor;
try {
if (hasPropertyEditorRegistryAccessor) {
accessor = errors.getClass().getMethod("getPropertyEditorRegistry", null);
}
else {
// only way to get at the registry in 1.2.8 or <.
accessor = errors.getClass().getDeclaredMethod("getBeanWrapper", null);
accessor.setAccessible(true);
}
}
catch (NoSuchMethodException e) {
throw new IllegalStateException(
"Unable to resolve property editor registry accessor method as expected - this should not happen");
}
return (PropertyEditorRegistry)ReflectionUtils.invokeMethod(accessor, errors);
}
/**
* Invoke specified validator method on the validator registered with this
* action. The validator method for piecemeal validation should have the
* following signature:
*
* public void ${validateMethodName}(${formObjectClass}, Errors)
*
* @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}.
*
* 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 final 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}.
*
* 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 final Errors getFormErrors(RequestContext context) throws Exception {
Object formObject = getFormObject(context);
if (!formErrorsExposed(context, formObject)) {
initFormErrors(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.
*
* 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
* @see #initBinder(RequestContext, DataBinder)
* @see #setMessageCodesResolver(MessageCodesResolver)
*/
protected DataBinder createBinder(RequestContext context, Object formObject) {
DataBinder binder = new WebDataBinder(formObject, getFormObjectName());
if (messageCodesResolver != null) {
binder.setMessageCodesResolver(messageCodesResolver);
}
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
*/
protected void doBind(RequestContext context, DataBinder binder) {
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.getErrors().getErrorCount() + "] errors, details: "
+ binder.getErrors().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 validate()
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(validator, "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 " + validator);
}
invokeValidatorMethod(validatorMethodName, formObject, errors);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Invoking validator " + validator);
}
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.
*
* Subclasses should override if they need to load the form object from a
* specific location or resource such as a database or filesystem.
*
* 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 (formObjectClass == 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 [" + formObjectClass + "]");
}
return formObjectClass.newInstance();
}
/**
* Initialize the new binder instance. This hook allows customization of
* binder settings such as the {@link DataBinder#getAllowedFields() allowed fields}
* and {@link DataBinder#getRequiredFields() required fields}. Called by
* {@link #createBinder(RequestContext, Object)}
*
* Note that registration of custom property editors should be done in
* {@link #registerPropertyEditors(PropertyEditorRegistry)}, not here! This
* method will only be called when a new data binder is created, e.g. when
* {@link #bind(RequestContext) data binding} needs to be done. In other cases,
* e.g. {@link #setupForm(RequestContext) form setup}, it will not be
* called since no data binder is required.
* @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.
*
* 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.
*
* This default implementation will call the
* {@link #registerPropertyEditors(PropertyEditorRegistry) simpler form} of
* the method not taking a RequestContext.
* @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.
*
* 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.
*
* This default implementation will simply call registerCustomEditors
* 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 (propertyEditorRegistrar != null) {
if (logger.isDebugEnabled()) {
logger.debug("Registering custom property editors using configured registrar");
}
propertyEditorRegistrar.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;
}
}