/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.wicket.markup.html.form;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import org.apache.wicket.Component;
import org.apache.wicket.IRequestTarget;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.Page;
import org.apache.wicket.Request;
import org.apache.wicket.RequestCycle;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.MarkupStream;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.border.Border;
import org.apache.wicket.markup.html.form.persistence.CookieValuePersister;
import org.apache.wicket.markup.html.form.persistence.IValuePersister;
import org.apache.wicket.markup.html.form.upload.FileUploadField;
import org.apache.wicket.markup.html.form.validation.IFormValidator;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.protocol.http.RequestUtils;
import org.apache.wicket.protocol.http.WebRequest;
import org.apache.wicket.protocol.http.WebRequestCycle;
import org.apache.wicket.request.IRequestCycleProcessor;
import org.apache.wicket.request.RequestParameters;
import org.apache.wicket.request.target.component.listener.ListenerInterfaceRequestTarget;
import org.apache.wicket.util.lang.Bytes;
import org.apache.wicket.util.string.AppendingStringBuffer;
import org.apache.wicket.util.string.Strings;
import org.apache.wicket.util.string.interpolator.MapVariableInterpolator;
import org.apache.wicket.util.upload.FileUploadException;
import org.apache.wicket.util.upload.FileUploadBase.SizeLimitExceededException;
import org.apache.wicket.util.value.ValueMap;
import org.apache.wicket.validation.IValidatorAddListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base class for forms. To implement a form, subclass this class, add
* FormComponents (such as CheckBoxes, ListChoices or TextFields) to the form.
* You can nest multiple buttons if you want to vary submit behavior. However,
* it is not necessary to use Wicket's button class, just putting e.g. <input
* type="submit" value="go"> suffices.
* <p>
* By default, the processing of a form works like this:
* <li> The submitting button is looked up. A submitting button is a button that
* is nested in this form (is a child component) and that was clicked by the
* user. If a submitting button was found, and it has the defaultFormProcessing
* field set to false (default is true), it's onSubmit method will be called
* right away, thus no validition is done, and things like updating form
* component models that would normally be done are skipped. In that respect,
* nesting a button with the defaultFormProcessing field set to false has the
* same effect as nesting a normal link. If you want you can call validate() to
* execute form validation, hasError() to find out whether validate() resulted
* in validation errors, and updateFormComponentModels() to update the models of
* nested form components. </li>
* <li> When no submitting button with defaultFormProcessing set to false was
* found, this form is processed (method process()). Now, two possible paths
* exist:
* <ul>
* <li> Form validation failed. All nested form components will be marked
* invalid, and onError() is called to allow clients to provide custom error
* handling code. </li>
* <li> Form validation succeeded. The nested components will be asked to update
* their models and persist their data is applicable. After that, method
* delegateSubmit with optionally the submitting button is called. The default
* when there is a submitting button is to first call onSubmit on that button,
* and after that call onSubmit on this form. Clients may override
* delegateSubmit if they want different behavior. </li>
* </ul>
* </li>
* </li>
* </p>
*
* Form for handling (file) uploads with multipart requests is supported by
* callign setMultiPart(true) ( although wicket will try to automatically detect
* this for you ). Use this with
* {@link org.apache.wicket.markup.html.form.upload.FileUploadField} components.
* You can attach mutliple FileUploadField components for muliple file uploads.
* <p>
* In case of an upload error two resource keys are available to specify error
* messages: uploadTooLarge and uploadFailed
*
* ie in [page].properties
*
* [form-id].uploadTooLarge=You have uploaded a file that is over the allowed
* limit of 2Mb
*
* <p>
* If you want to have multiple buttons which submit the same form, simply put
* two or more button components somewhere in the hierarchy of components that
* are children of the form.
* </p>
* <p>
* To get form components to persist their values for users via cookies, simply
* call setPersistent(true) on each component.
* </p>
* <p>
* Forms can be nested. You can put a form in another form. Since HTML doesn't
* allow nested <form> tags, the inner forms will be rendered using the
* <div> tag. You have to submit the inner forms using explicit components
* (like Button or SubmitLink), you can't rely on implicit submit behavior (by
* using just <input type="submit"> that is not attached to a component).
* </p>
* <p>
* When a nested form is submitted, the user entered values in outer (parent)
* forms are preserved and only the fields in the submitted form are validated.
* </b>
*
* @author Jonathan Locke
* @author Juergen Donnerstag
* @author Eelco Hillenius
* @author Cameron Braid
* @author Johan Compagner
* @author Igor Vaynberg (ivaynberg)
*/
public class Form extends WebMarkupContainer implements IFormSubmitListener
{
/**
* Visitor used for validation
*
* @author Igor Vaynberg (ivaynberg)
*/
public static abstract class ValidationVisitor implements FormComponent.IVisitor
{
/**
* @see org.apache.wicket.markup.html.form.FormComponent.IVisitor#formComponent(org.apache.wicket.markup.html.form.IFormVisitorParticipant)
*/
public Object formComponent(IFormVisitorParticipant component)
{
if (component instanceof FormComponent)
{
FormComponent formComponent = (FormComponent)component;
if (formComponent.isVisibleInHierarchy() && formComponent.isValid()
&& formComponent.isEnabled() && formComponent.isEnableAllowed())
{
validate(formComponent);
}
}
if (component.processChildren())
{
return Component.IVisitor.CONTINUE_TRAVERSAL;
}
else
{
return Component.IVisitor.CONTINUE_TRAVERSAL_BUT_DONT_GO_DEEPER;
}
}
/**
* Callback that should be used to validate form component
*
* @param formComponent
*/
public abstract void validate(FormComponent formComponent);
}
/**
*
*/
class FormDispatchRequest extends Request
{
private final ValueMap params = new ValueMap();
private final Request realRequest;
private final String url;
/**
* Construct.
*
* @param realRequest
* @param url
*/
public FormDispatchRequest(final Request realRequest, final String url)
{
this.realRequest = realRequest;
this.url = realRequest.decodeURL(url);
String queryString = this.url.substring(this.url.indexOf("?") + 1);
RequestUtils.decodeParameters(queryString, params);
}
/**
* @see org.apache.wicket.Request#getLocale()
*/
public Locale getLocale()
{
return realRequest.getLocale();
}
/**
* @see org.apache.wicket.Request#getParameter(java.lang.String)
*/
public String getParameter(String key)
{
return (String)params.get(key);
}
/**
* @see org.apache.wicket.Request#getParameterMap()
*/
public Map getParameterMap()
{
return params;
}
/**
* @see org.apache.wicket.Request#getParameters(java.lang.String)
*/
public String[] getParameters(String key)
{
String param = (String)params.get(key);
if (param != null)
{
return new String[] { param };
}
return new String[0];
}
/**
* @see org.apache.wicket.Request#getPath()
*/
public String getPath()
{
return realRequest.getPath();
}
public String getRelativePathPrefixToContextRoot()
{
return realRequest.getRelativePathPrefixToContextRoot();
}
public String getRelativePathPrefixToWicketHandler()
{
return realRequest.getRelativePathPrefixToWicketHandler();
}
/**
* @see org.apache.wicket.Request#getURL()
*/
public String getURL()
{
return url;
}
}
/**
* Constant for specifying how a form is submitted, in this case using get.
*/
public static final String METHOD_GET = "get";
/**
* Constant for specifying how a form is submitted, in this case using post.
*/
public static final String METHOD_POST = "post";
/** Flag that indicates this form has been submitted during this request */
private static final short FLAG_SUBMITTED = FLAG_RESERVED1;
/** Log. */
private static final Logger log = LoggerFactory.getLogger(Form.class);
private static final long serialVersionUID = 1L;
private static final String UPLOAD_FAILED_RESOURCE_KEY = "uploadFailed";
private static final String UPLOAD_TOO_LARGE_RESOURCE_KEY = "uploadTooLarge";
/**
* Any default button. If set, a hidden submit button will be rendered right
* after the form tag, so that when users press enter in a textfield, this
* button's action will be selected. If no default button is set, nothing
* additional is rendered.
* <p>
* WARNING: note that this is a best effort only. Unfortunately having a
* 'default' button in a form is ill defined in the standards, and of course
* IE has it's own way of doing things.
* </p>
*/
private Button defaultButton;
/** multi-validators assigned to this form */
private Object formValidators = null;
private String javascriptId;
/** Maximum size of an upload in bytes */
private Bytes maxSize = Bytes.MAX;
/** True if the form has enctype of multipart/form-data */
private boolean multiPart = false;
/**
* Constructs a form with no validation.
*
* @param id
* See Component
*/
public Form(final String id)
{
super(id);
}
/**
* @param id
* See Component
* @param model
* See Component
* @see org.apache.wicket.Component#Component(String, IModel)
*/
public Form(final String id, IModel model)
{
super(id, model);
}
/**
* Adds a form validator to the form.
*
* @param validator
* validator
* @throws IllegalArgumentException
* if validator is null
* @see IFormValidator
* @see IValidatorAddListener
*/
public void add(IFormValidator validator)
{
if (validator == null)
{
throw new IllegalArgumentException("validator argument cannot be null");
}
// add the validator
formValidators_add(validator);
// see whether the validator listens for add events
if (validator instanceof IValidatorAddListener)
{
((IValidatorAddListener)validator).onAdded(this);
}
}
/**
* Clears the input from the form's nested children of type
* {@link FormComponent}. This method is typically called when a form needs
* to be reset.
*/
public final void clearInput()
{
// Visit all the (visible) form components and clear the input on each.
visitFormComponentsPostOrder(new FormComponent.AbstractVisitor()
{
public void onFormComponent(final FormComponent formComponent)
{
if (formComponent.isVisibleInHierarchy())
{
// Clear input from form component
formComponent.clearInput();
}
}
});
}
/**
* /** Registers an error feedback message for this component
*
* @param error
* error message
* @param args
* argument replacement map for ${key} variables
*/
public final void error(String error, Map args)
{
error(new MapVariableInterpolator(error, args).toString());
}
/**
* Gets the button which submitted this form.
*
* @return The button which submitted this form or null if the processing
* was not trigger by a registered button component
*/
public final IFormSubmittingComponent findSubmittingButton()
{
IFormSubmittingComponent submittingButton = (IFormSubmittingComponent)getPage()
.visitChildren(IFormSubmittingComponent.class, new IVisitor()
{
public Object component(final Component component)
{
// Get button
final IFormSubmittingComponent submit = (IFormSubmittingComponent)component;
// Check for button-name or button-name.x request string
if (submit.getForm() != null
&& submit.getForm().getRootForm() == Form.this
&& (getRequest().getParameter(submit.getInputName()) != null || getRequest()
.getParameter(submit.getInputName() + ".x") != null))
{
if (!component.isVisible())
{
throw new WicketRuntimeException("Submit Button "
+ submit.getInputName() + " (path="
+ component.getPageRelativePath() + ") is not visible");
}
return submit;
}
return CONTINUE_TRAVERSAL;
}
});
return submittingButton;
}
/**
* Gets the default button. If set (not null), a hidden submit button will
* be rendered right after the form tag, so that when users press enter in a
* textfield, this button's action will be selected. If no default button is
* set (it is null), nothing additional is rendered.
* <p>
* WARNING: note that this is a best effort only. Unfortunately having a
* 'default' button in a form is ill defined in the standards, and of course
* IE has it's own way of doing things.
* </p>
* There can be only one default button per form hierarchy. So if you want
* to get the default button on a nested form, it will actually delegate the
* call to root form. </b>
*
* @return The button to set as the default button, or null when you want to
* 'unset' any previously set default button
*/
public final Button getDefaultButton()
{
if (isRootForm())
{
return defaultButton;
}
else
{
return getRootForm().getDefaultButton();
}
}
/**
* This generates a piece of javascript code that sets the url in the
* special hidden field and submits the form.
*
* Warning: This code should only be called in the rendering phase for form
* components inside the form because it uses the css/javascript id of the
* form which can be stored in the markup.
*
* @param url
* The interface url that has to be stored in the hidden field
* and submitted
* @return The javascript code that submits the form.
*/
public final CharSequence getJsForInterfaceUrl(CharSequence url)
{
return new AppendingStringBuffer("document.getElementById('").append(getHiddenFieldId())
.append("').value='").append(url).append("';document.getElementById('").append(
getJavascriptId()).append("').submit();");
}
/**
* @return the maxSize of uploaded files
*/
public Bytes getMaxSize()
{
return this.maxSize;
}
/**
* Returns the root form or this, if this is the root form.
*
* @return root form or this form
*/
public Form getRootForm()
{
Form form;
Form parent = this;
do
{
form = parent;
parent = (Form)form.findParent(Form.class);
}
while (parent != null);
return form;
}
/**
* Returns the prefix used when building validator keys. This allows a form
* to use a separate "set" of keys. For example if prefix "short" is
* returned, validator key short.Required will be tried instead of Required
* key.
* <p>
* This can be useful when different designs are used for a form. In a form
* where error messages are displayed next to their respective form
* components as opposed to at the top of the form, the ${label} attribute
* is of little use and only causes redundant information to appear in the
* message. Forms like these can return the "short" (or any other string)
* validator prefix and declare key: short.Required=required to override the
* longer message which is usually declared like this: Required=${label} is
* a required field
* <p>
* Returned prefix will be used for all form components. The prefix can also
* be overridden on form component level by overriding
* {@link FormComponent#getValidatorKeyPrefix()}
*
* @return prefix prepended to validator keys
*/
public String getValidatorKeyPrefix()
{
return null;
}
/**
* Gets whether the current form has any error registered.
*
* @return True if this form has at least one error.
*/
public final boolean hasError()
{
// if this form itself has an error message
if (hasErrorMessage())
{
return true;
}
// the form doesn't have any errors, now check any nested form
// components
return anyFormComponentError();
}
/**
* Returns whether the form is a root form, which means that there's no
* other form in it's parent hierarchy.
*
* @return true if form is a root form, false otherwise
*/
public boolean isRootForm()
{
return findParent(Form.class) == null;
}
/**
* Checks if this form has been submitted during the current request
*
* @return true if the form has been submitted during this request, false
* otherwise
*/
public final boolean isSubmitted()
{
return getFlag(FLAG_SUBMITTED);
}
/**
* Method made final because we want to ensure users call setVersioned.
*
* @see org.apache.wicket.Component#isVersioned()
*/
public boolean isVersioned()
{
return super.isVersioned();
}
/**
* THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL IT.
* <p>
* Retrieves FormComponent values related to the page using the persister
* and assign the values to the FormComponent. Thus initializing them.
*/
public final void loadPersistentFormComponentValues()
{
visitFormComponentsPostOrder(new FormComponent.AbstractVisitor()
{
public void onFormComponent(final FormComponent formComponent)
{
// Component must implement persister interface and
// persistence for that component must be enabled.
// Else ignore the persisted value. It'll be deleted
// once the user submits the Form containing that FormComponent.
// Note: if that is true, values may remain persisted longer
// than really necessary
if (formComponent.isVisibleInHierarchy() && formComponent.isPersistent())
{
// The persister
final IValuePersister persister = getValuePersister();
// Retrieve persisted value
persister.load(formComponent);
}
}
});
}
/**
* THIS METHOD IS NOT PART OF THE WICKET API. DO NOT ATTEMPT TO OVERRIDE OR
* CALL IT.
*
* Handles form submissions.
*
* @see Form#validate()
*/
public final void onFormSubmitted()
{
setFlag(FLAG_SUBMITTED, true);
if (handleMultiPart())
{
// Tells FormComponents that a new user input has come
inputChanged();
String url = getRequest().getParameter(getHiddenFieldId());
if (!Strings.isEmpty(url))
{
dispatchEvent(getPage(), url);
}
else
{
// First, see if the processing was triggered by a Wicket button
final IFormSubmittingComponent submittingButton = findSubmittingButton();
// When processing was triggered by a Wicket button and that
// button indicates it wants to be called immediately
// (without processing), call Button.onSubmit() right away.
if (submittingButton != null && !submittingButton.getDefaultFormProcessing())
{
submittingButton.onSubmit();
}
else
{
// this is the root form
Form formToProcess = this;
// find out whether it was a nested form that was submitted
if (submittingButton != null)
{
formToProcess = submittingButton.getForm();
}
// process the form for this request
if (formToProcess.process())
{
// let clients handle further processing
delegateSubmit(submittingButton);
}
}
}
}
// If multi part did fail check if an error is registered and call
// onError
else if (hasError())
{
onError();
}
}
/**
* Process the form. Though you can override this method to provide your
* whole own algorithm, it is not recommended to do so.
* <p>
* See the class documentation for further details on the form processing
* </p>
*
* @return False if the form had an error
*/
public boolean process()
{
// run validation
validate();
// If a validation error occurred
if (hasError())
{
// mark all children as invalid
markFormComponentsInvalid();
// let subclass handle error
onError();
// Form has an error
return false;
}
else
{
// mark all childeren as valid
markFormComponentsValid();
// before updating, call the interception method for clients
beforeUpdateFormComponentModels();
// Update model using form data
updateFormComponentModels();
// Persist FormComponents if requested
persistFormComponentData();
// Form has no error
return true;
}
}
/**
* Removes already persisted data for all FormComponent childs and disable
* persistence for the same components.
*
* @see Page#removePersistedFormData(Class, boolean)
*
* @param disablePersistence
* if true, disable persistence for all FormComponents on that
* page. If false, it will remain unchanged.
*/
public void removePersistentFormComponentValues(final boolean disablePersistence)
{
// The persistence manager responsible to persist and retrieve
// FormComponent data
final IValuePersister persister = getValuePersister();
// Search for FormComponents like TextField etc.
visitFormComponentsPostOrder(new FormComponent.AbstractVisitor()
{
public void onFormComponent(final FormComponent formComponent)
{
if (formComponent.isVisibleInHierarchy())
{
// remove the FormComponent's persisted data
persister.clear(formComponent);
// Disable persistence if requested. Leave unchanged
// otherwise.
if (formComponent.isPersistent() && disablePersistence)
{
formComponent.setPersistent(false);
}
}
}
});
}
/**
* Sets the default button. If set (not null), a hidden submit button will
* be rendered right after the form tag, so that when users press enter in a
* textfield, this button's action will be selected. If no default button is
* set (so unset by calling this method with null), nothing additional is
* rendered.
* <p>
* WARNING: note that this is a best effort only. Unfortunately having a
* 'default' button in a form is ill defined in the standards, and of course
* IE has it's own way of doing things.
* </p>
* There can be only one default button per form hierarchy. So if you set
* default button on a nested form, it will actually delegate the call to
* root form. </b>
*
* @param button
* The button to set as the default button, or null when you want
* to 'unset' any previously set default button
*/
public final void setDefaultButton(Button button)
{
if (isRootForm())
{
this.defaultButton = button;
}
else
{
getRootForm().setDefaultButton(button);
}
}
/**
* @param maxSize
* The maxSize for uploaded files
*/
public void setMaxSize(final Bytes maxSize)
{
this.maxSize = maxSize;
}
/**
* Set to true to use enctype='multipart/form-data', and to process file
* uplloads by default multiPart = false
*
* @param multiPart
* whether this form should behave as a multipart form
*/
public void setMultiPart(boolean multiPart)
{
this.multiPart = multiPart;
}
/**
* @see org.apache.wicket.Component#setVersioned(boolean)
*/
public final Component setVersioned(final boolean isVersioned)
{
super.setVersioned(isVersioned);
// Search for FormComponents like TextField etc.
visitFormComponents(new FormComponent.AbstractVisitor()
{
public void onFormComponent(final FormComponent formComponent)
{
formComponent.setVersioned(isVersioned);
}
});
return this;
}
/**
* Convenient and typesafe way to visit all the form components on a form
*
* @param visitor
* The visitor interface to call
*/
public final void visitFormComponents(final FormComponent.IVisitor visitor)
{
visitChildren(FormComponent.class, new IVisitor()
{
public Object component(final Component component)
{
visitor.formComponent((FormComponent)component);
return CONTINUE_TRAVERSAL;
}
});
/**
* TODO Post 1.2 General: Maybe we should re-think how Borders are
* implemented, because there are just too many exceptions in the code
* base because of borders. This time it is to solve the problem tested
* in BoxBorderTestPage_3 where the Form is defined in the box border
* and the FormComponents are in the "body". Thus, the formComponents
* are not childs of the form. They are rather childs of the border, as
* the Form itself.
*/
if (getParent() instanceof Border)
{
MarkupContainer border = getParent();
Iterator iter = border.iterator();
while (iter.hasNext())
{
Component child = (Component)iter.next();
if (child instanceof FormComponent)
{
visitor.formComponent((FormComponent)child);
}
}
}
}
/**
* Convenient and typesafe way to visit all the form components on a form
* postorder (deepest first)
*
* @param visitor
* The visitor interface to call
*/
public final void visitFormComponentsPostOrder(final FormComponent.IVisitor visitor)
{
FormComponent.visitFormComponentsPostOrder(this, visitor);
/**
* TODO Post 1.2 General: Maybe we should re-think how Borders are
* implemented, because there are just too many exceptions in the code
* base because of borders. This time it is to solve the problem tested
* in BoxBorderTestPage_3 where the Form is defined in the box border
* and the FormComponents are in the "body". Thus, the formComponents
* are not childs of the form. They are rather childs of the border, as
* the Form itself.
*/
if (getParent() instanceof Border)
{
MarkupContainer border = getParent();
Iterator iter = border.iterator();
while (iter.hasNext())
{
Component child = (Component)iter.next();
if (child instanceof FormComponent)
{
visitor.formComponent((FormComponent)child);
}
}
}
}
/**
* Find out whether there is any registered error for a form component.
*
* @return whether there is any registered error for a form component
*/
private boolean anyFormComponentError()
{
final Object value = visitChildren(new IVisitor()
{
public Object component(final Component component)
{
if (component.hasErrorMessage())
{
return STOP_TRAVERSAL;
}
// Traverse all children
return CONTINUE_TRAVERSAL;
}
});
return value == IVisitor.STOP_TRAVERSAL ? true : false;
}
/**
* Method for dispatching/calling a interface on a page from the given url.
* Used by {@link org.apache.wicket.markup.html.form.Form#onFormSubmitted()}
* for dispatching events
*
* @param page
* The page where the event should be called on.
* @param url
* The url which describes the component path and the interface
* to be called.
*/
private void dispatchEvent(final Page page, final String url)
{
RequestCycle rc = RequestCycle.get();
IRequestCycleProcessor processor = rc.getProcessor();
final RequestParameters requestParameters = processor.getRequestCodingStrategy().decode(
new FormDispatchRequest(rc.getRequest(), url));
IRequestTarget rt = processor.resolve(rc, requestParameters);
if (rt instanceof ListenerInterfaceRequestTarget)
{
ListenerInterfaceRequestTarget interfaceTarget = ((ListenerInterfaceRequestTarget)rt);
interfaceTarget.getRequestListenerInterface().invoke(page, interfaceTarget.getTarget());
}
else
{
throw new WicketRuntimeException(
"Attempt to access unknown request listener interface "
+ requestParameters.getInterfaceName());
}
}
/**
* @param validator
* The form validator to add to the formValidators Object (which
* may be an array of IFormValidators or a single instance, for
* efficiency)
*/
private void formValidators_add(final IFormValidator validator)
{
if (this.formValidators == null)
{
this.formValidators = validator;
}
else
{
// Get current list size
final int size = formValidators_size();
// Create array that holds size + 1 elements
final IFormValidator[] validators = new IFormValidator[size + 1];
// Loop through existing validators copying them
for (int i = 0; i < size; i++)
{
validators[i] = formValidators_get(i);
}
// Add new validator to the end
validators[size] = validator;
// Save new validator list
this.formValidators = validators;
}
}
/**
* Gets form validator from formValidators Object (which may be an array of
* IFormValidators or a single instance, for efficiency) at the given index
*
* @param index
* The index of the validator to get
* @return The form validator
*/
private IFormValidator formValidators_get(int index)
{
if (this.formValidators == null)
{
throw new IndexOutOfBoundsException();
}
if (this.formValidators instanceof IFormValidator[])
{
return ((IFormValidator[])formValidators)[index];
}
return (IFormValidator)formValidators;
}
/**
* @return The number of form validators in the formValidators Object (which
* may be an array of IFormValidators or a single instance, for
* efficiency)
*/
private int formValidators_size()
{
if (this.formValidators == null)
{
return 0;
}
if (this.formValidators instanceof IFormValidator[])
{
return ((IFormValidator[])formValidators).length;
}
return 1;
}
/**
* Visits the form's children FormComponents and inform them that a new user
* input is available in the Request
*/
private void inputChanged()
{
visitFormComponentsPostOrder(new FormComponent.AbstractVisitor()
{
public void onFormComponent(final FormComponent formComponent)
{
if (formComponent.isVisibleInHierarchy())
{
formComponent.inputChanged();
}
}
});
}
/**
* Persist (e.g. Cookie) FormComponent data to be reloaded and re-assigned
* to the FormComponent automatically when the page is visited by the user
* next time.
*
* @see org.apache.wicket.markup.html.form.FormComponent#updateModel()
*/
private void persistFormComponentData()
{
// Cannot add cookies to request cycle unless it accepts them
// We could conceivably be HTML over some other protocol!
if (getRequestCycle() instanceof WebRequestCycle)
{
// The persistence manager responsible to persist and retrieve
// FormComponent data
final IValuePersister persister = getValuePersister();
// Search for FormComponent children. Ignore all other
visitFormComponentsPostOrder(new FormComponent.AbstractVisitor()
{
public void onFormComponent(final FormComponent formComponent)
{
if (formComponent.isVisibleInHierarchy())
{
// If peristence is switched on for that FormComponent
// ...
if (formComponent.isPersistent())
{
// Save component's data (e.g. in a cookie)
persister.save(formComponent);
}
else
{
// Remove component's data (e.g. cookie)
persister.clear(formComponent);
}
}
}
});
}
}
/**
* If a default button was set on this form, this method will be called to
* render an extra field with an invisible style so that pressing enter in
* one of the textfields will do a form submit using this button. This
* method is overridable as what we do is best effort only, and may not what
* you want in specific situations. So if you have specific usability
* concerns, or want to follow another strategy, you may override this
* method.
*
* @param markupStream
* The markup stream
* @param openTag
* The open tag for the body
*/
protected void appendDefaultButtonField(final MarkupStream markupStream,
final ComponentTag openTag)
{
String nameAndId = getHiddenFieldId();
AppendingStringBuffer buffer = new AppendingStringBuffer();
// get the value, first seeing whether the value attribute is set
// by a model
String value = defaultButton.getModelObjectAsString();
if (value == null || "".equals(value))
{
// nope it isn't; try to read from the attributes
// note that we're only trying lower case here
value = defaultButton.getMarkupAttributes().getString("value");
}
// append the button
buffer.append("<input type=\"submit\" value=\"").append(value).append("\" name=\"").append(
defaultButton.getInputName()).append("\"");
buffer.append("style=\"width: 0px; height: 0px; position: absolute; left:-100px;\"");
buffer.append(" />");
getResponse().write(buffer);
}
/**
* Template method to allow clients to do any processing (like recording the
* current model so that, in case onSubmit does further validation, the
* model can be rolled back) before the actual updating of form component
* models is done.
*/
protected void beforeUpdateFormComponentModels()
{
}
/**
* Called (by the default implementation of 'process') when all fields
* validated, the form was updated and it's data was allowed to be
* persisted. It is meant for delegating further processing to clients.
* <p>
* This implementation first finds out whether the form processing was
* triggered by a nested button of this form. If that is the case, that
* button's onSubmit is called first.
* </p>
* <p>
* Regardless of whether a submitting button was found, the form's onSubmit
* method is called next.
* </p>
*
* @param submittingButton
* the button that triggered this form processing, or null if the
* processing was triggered by something else (like a non-Wicket
* submit button or a javascript execution)
*/
protected void delegateSubmit(IFormSubmittingComponent submittingButton)
{
// when the given button is not null, it means that it was the
// submitting button
if (submittingButton != null)
{
submittingButton.onSubmit();
}
// Model was successfully updated with valid data
onSubmit();
}
/**
* Returns the HiddenFieldId which will be used as the name and id property
* of the hiddenfield that is generated for event dispatches.
*
* @return The name and id of the hidden field.
*/
protected final String getHiddenFieldId()
{
return getJavascriptId() + "_hf_0";
}
/**
* Returns the javascript/css id of this form that will be used to generated
* the id="xxx" attribute. it will be generated if not set already in the
* onComponentTag. Where it will be tried to load from the markup first
* before it is generated.
*
* @return The javascript/css id of this form.
*/
protected final String getJavascriptId()
{
if (Strings.isEmpty(javascriptId))
{
javascriptId = getMarkupId();
}
return javascriptId;
}
/**
* Gets the method used to submit the form. Defaults to 'post'. Override
* this if you have a requirement to alter this behavior.
*
* @return the method used to submit the form.
*/
protected String getMethod()
{
return METHOD_POST;
}
protected boolean getStatelessHint()
{
return false;
}
/**
* Gets the form component persistence manager; it is lazy loaded.
*
* @return The form component value persister
*/
protected IValuePersister getValuePersister()
{
return new CookieValuePersister();
}
/**
* Handles multi-part processing of the submitted data.
*
* WARNING
*
* If this method is overridden it can break {@link FileUploadField}s on
* this form
*
* @return false if form is multipart and upload failed
*/
protected boolean handleMultiPart()
{
if (multiPart)
{
// Change the request to a multipart web request so parameters are
// parsed out correctly
try
{
final WebRequest multipartWebRequest = ((WebRequest)getRequest())
.newMultipartWebRequest(this.maxSize);
getRequestCycle().setRequest(multipartWebRequest);
}
catch (WicketRuntimeException wre)
{
if (wre.getCause() == null || !(wre.getCause() instanceof FileUploadException))
{
throw wre;
}
FileUploadException e = (FileUploadException)wre.getCause();
// Create model with exception and maximum size values
final Map model = new HashMap();
model.put("exception", e);
model.put("maxSize", maxSize);
if (e instanceof SizeLimitExceededException)
{
// Resource key should be <form-id>.uploadTooLarge to
// override default message
final String defaultValue = "Upload must be less than " + maxSize;
String msg = getString(getId() + "." + UPLOAD_TOO_LARGE_RESOURCE_KEY, Model
.valueOf(model), defaultValue);
error(msg);
if (log.isDebugEnabled())
{
log.error(msg, e);
}
else
{
log.error(msg);
}
}
else
{
// Resource key should be <form-id>.uploadFailed to override
// default message
final String defaultValue = "Upload failed: " + e.getLocalizedMessage();
String msg = getString(getId() + "." + UPLOAD_FAILED_RESOURCE_KEY, Model
.valueOf(model), defaultValue);
error(msg);
log.error(msg, e);
}
// don't process the form if there is a FileUploadException
return false;
}
}
return true;
}
/**
* @see org.apache.wicket.Component#internalOnModelChanged()
*/
protected void internalOnModelChanged()
{
// Visit all the form components and validate each
visitFormComponentsPostOrder(new FormComponent.AbstractVisitor()
{
public void onFormComponent(final FormComponent formComponent)
{
// If form component is using form model
if (formComponent.sameInnermostModel(Form.this))
{
formComponent.modelChanged();
}
}
});
}
/**
* Mark each form component on this form invalid.
*/
protected final void markFormComponentsInvalid()
{
// call invalidate methods of all nested form components
visitFormComponentsPostOrder(new FormComponent.AbstractVisitor()
{
public void onFormComponent(final FormComponent formComponent)
{
if (formComponent.isVisibleInHierarchy())
{
formComponent.invalid();
}
}
});
}
/**
* Mark each form component on this form valid.
*/
protected final void markFormComponentsValid()
{
// call invalidate methods of all nested form components
visitFormComponentsPostOrder(new FormComponent.AbstractVisitor()
{
public void onFormComponent(final FormComponent formComponent)
{
if (formComponent.isVisibleInHierarchy())
{
formComponent.valid();
}
}
});
}
/**
* @see org.apache.wicket.Component#onComponentTag(ComponentTag)
*/
protected void onComponentTag(final ComponentTag tag)
{
super.onComponentTag(tag);
checkComponentTag(tag, "form");
// If the javascriptid is already generated then use that on even it
// was before the first render. Because there could be a component
// which already uses it to submit the forum. This should be fixed
// when we pre parse the markup so that we know the id is at front.
if (!Strings.isEmpty(javascriptId))
{
tag.put("id", javascriptId);
}
else
{
javascriptId = (String)tag.getAttributes().get("id");
if (Strings.isEmpty(javascriptId))
{
javascriptId = getJavascriptId();
tag.put("id", javascriptId);
}
}
if (isRootForm())
{
tag.put("method", getMethod());
tag.put("action", Strings.replaceAll(urlFor(IFormSubmitListener.INTERFACE), "&",
"&"));
if (multiPart)
{
tag.put("enctype", "multipart/form-data");
}
else
{
// sanity check
String enctype = (String)tag.getAttributes().get("enctype");
if ("multipart/form-data".equalsIgnoreCase(enctype))
{
// though not set explicitly in Java, this is a multipart
// form
setMultiPart(true);
}
}
}
else
{
tag.setName("div");
tag.remove("method");
tag.remove("action");
tag.remove("enctype");
}
}
/**
* Append an additional hidden input tag to support anchor tags that can
* submit a form.
*
* @param markupStream
* The markup stream
* @param openTag
* The open tag for the body
*/
protected void onComponentTagBody(final MarkupStream markupStream, final ComponentTag openTag)
{
if (isRootForm())
{
// get the hidden field id
String nameAndId = getHiddenFieldId();
// render the hidden field
AppendingStringBuffer buffer = new AppendingStringBuffer(
"<div style=\"display:none\"><input type=\"hidden\" name=\"").append(nameAndId)
.append("\" id=\"").append(nameAndId).append("\" /></div>");
getResponse().write(buffer);
// if a default button was set, handle the rendering of that
if (defaultButton != null && defaultButton.isVisibleInHierarchy()
&& defaultButton.isEnabled())
{
appendDefaultButtonField(markupStream, openTag);
}
}
// do the rest of the processing
super.onComponentTagBody(markupStream, openTag);
}
/**
* @see org.apache.wicket.Component#onDetach()
*/
protected void onDetach()
{
super.internalOnDetach();
setFlag(FLAG_SUBMITTED, false);
super.onDetach();
}
/**
* Method to override if you want to do something special when an error
* occurs (other than simply displaying validation errors).
*/
protected void onError()
{
}
/**
* @see org.apache.wicket.Component#onRender(MarkupStream)
*/
protected void onRender(final MarkupStream markupStream)
{
// Force multi-part on if any child form component is multi-part
visitFormComponents(new FormComponent.AbstractVisitor()
{
public void onFormComponent(FormComponent formComponent)
{
if (formComponent.isVisible() && formComponent.isMultiPart())
{
setMultiPart(true);
}
}
});
super.onRender(markupStream);
}
/**
* Implemented by subclasses to deal with form submits.
*/
protected void onSubmit()
{
}
/**
* Update the model of all form components using the fields that were sent
* with the current request.
*
* @see org.apache.wicket.markup.html.form.FormComponent#updateModel()
*/
protected final void updateFormComponentModels()
{
visitFormComponentsPostOrder(new ValidationVisitor()
{
public void validate(FormComponent formComponent)
{
// Potentially update the model
formComponent.updateModel();
}
});
}
/**
* Validates the form by checking required fields, converting raw input and
* running validators for every form component, and last running global form
* validators. This method is typically called before updating any models.
* <p>
* NOTE: in most cases, custom validations on the form can be achieved using
* an IFormValidator that can be added using addValidator().
* </p>
*/
protected void validate()
{
validateComponents();
validateFormValidators();
}
/**
* Triggers type conversion on form components
*/
protected final void validateComponents()
{
visitFormComponentsPostOrder(new ValidationVisitor()
{
public void validate(final FormComponent formComponent)
{
formComponent.validate();
}
});
}
/**
* Validates form with the given form validator
*
* @param validator
*/
protected final void validateFormValidator(final IFormValidator validator)
{
if (validator == null)
{
throw new IllegalArgumentException("Argument [[validator]] cannot be null");
}
final FormComponent[] dependents = validator.getDependentFormComponents();
boolean validate = true;
if (dependents != null)
{
for (int j = 0; j < dependents.length; j++)
{
final FormComponent dependent = dependents[j];
if (!dependent.isValid())
{
validate = false;
break;
}
}
}
if (validate)
{
validator.validate(this);
}
}
/**
* Triggers any added {@link IFormValidator}s.
*/
protected final void validateFormValidators()
{
final int count = formValidators_size();
for (int i = 0; i < count; i++)
{
validateFormValidator(formValidators_get(i));
}
}
}