Java tutorial
/* * 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.click.control; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.click.Context; import org.apache.click.Control; import org.apache.click.Page; import org.apache.click.Stateful; import org.apache.click.element.CssImport; import org.apache.click.element.Element; import org.apache.click.element.JsImport; import org.apache.click.service.FileUploadService; import org.apache.click.service.LogService; import org.apache.click.util.ClickUtils; import org.apache.click.util.ContainerUtils; import org.apache.click.util.HtmlStringBuffer; import org.apache.commons.fileupload.FileUploadException; import org.apache.commons.fileupload.FileUploadBase.FileSizeLimitExceededException; import org.apache.commons.fileupload.FileUploadBase.SizeLimitExceededException; import org.apache.commons.lang.StringUtils; /** * Provides a Form control: <form method='post'>. * * <table class='htmlHeader' cellspacing='12'> * <tr> * <td> * * <table class='fields'> * <tr> * <td align='left'><label>Username</label><span class="red">*</span></td> * <td align='left'><input type='text' name='username' value='' size='20' maxlength='20' /></td> * </tr> * <tr> * <td align='left'><label>Password</label><span class="red">*</span></td> * <td align='left'><input type='password' name='password' value='' size='20' maxlength='20' /></td> * </tr> * </table> * <table class="buttons"> * <tr><td> * <input type='submit' name='ok' value=' OK '/> <input type='submit' name='cancel' value=' Cancel '/> * </td></tr> * </table> * * </td> * </tr> * </table> * * When a Form is processed it will process its {@link Field} controls * in the order they were added to the form, and then it will process the * {@link Button} controls in the added order. Once all the Fields have been * processed the form will invoke its action listener if defined. * * <h3>Form Example</h3> * * The example below illustrates a Form being used in a login Page. * * <pre class="prettyprint"> * public class Login extends Page { * * public Form form = new Form(); * * public Login() { * form.add(new TextField("username", true)); * form.add(new PasswordField("password", true)); * form.add(new Submit("ok", " OK ", this, "onOkClick")); * form.add(new Submit("cancel", this, "onCancelClick")); * } * * public boolean onOkClick() { * if (form.isValid()) { * User user = new User(); * form.copyTo(user); * * if (getUserService().isAuthenticatedUser(user)) { * getContext().setSessionAttribute("user", user); * setRedirect(HomePage.class); * } else { * form.setError(getMessage("authentication-error")); * } * } * return true; * } * * public boolean onCancelClick() { * setRedirect(WelcomePage.class); * return false; * } * } </pre> * * The forms corresponding template code is below. Note the form automatically * renders itself when Velocity invokes its {@link #toString()} method. * * <pre class="codeHtml"> * <span class="blue">$form</span> </pre> * * If a Form has been posted and processed, if it has an {@link #error} defined or * any of its Fields have validation errors they will be automatically * rendered, and the {@link #isValid()} method will return false. * * <a name="data-binding"></a> * <h3>Data Binding</h3> * * To bind value objects to a forms fields use the copy methods: * <ul> * <li>value object -> form fields * {@link #copyFrom(Object)}</li> * <li>form fields -> value object * {@link #copyTo(Object)}</li> * </ul> * To debug the data binding being performed, use the Click application mode to * "<tt>debug</tt>" or use the debug copy methods. * <p/> * Binding of nested data objects is supported using the * <a target="blank" href="http://www.ognl.org">OGNL</a> library. To use * nested objects in your form, simply specify the object path as the Field * name. Note in the object path you exclude the root object, so the path * <tt>customer.address.state</tt> is specified as <tt>address.state</tt>. * <p/> * For example: * * <pre class="prettyprint"> * // The customer.address.state field * TextField stateField = new TextField("address.state"); * form.add(stateField); * .. * * // Loads the customer address state into the form stateField * Customer customer = getCustomer(); * form.copyFrom(customer); * .. * * // Copies form stateField value into the customer address state * Customer customer = new Customer(); * form.copyTo(customer); </pre> * * When populating an object from a form post Click will automatically create * any null nested objects so their properties can be set. To do this Click * uses the no-args constructor of the nested objects class. * <p/> * {@link #copyTo(Object)} and {@link #copyFrom(Object)} also supports * <tt>java.util.Map</tt> as an argument. Examples of using * <tt>java.util.Map</tt> are shown in the respective method descriptions. * * <a name="form-validation"></a> * <h3>Form Validation</h3> * * The Form control supports automatic field validation. By default when a POST * request is made the form will validate the field values. To disable * automatic validation set {@link #setValidate(boolean)} to false. * <p/> * Form also provides a {@link #validate()} method where subclasses can provide * custom cross-field validation. * <p/> * <b>File Upload Validation</b> * <p/> * The Form's {@link #validateFileUpload()} provides validation for multipart * requests (multipart requests are used for uploading files from the browser). * The {@link #validateFileUpload()} method checks that files being uploaded do not exceed the * {@link org.apache.click.service.CommonsFileUploadService#sizeMax maximum request size} * or the {@link org.apache.click.service.CommonsFileUploadService#fileSizeMax maximum file size}. * <p/> * <b>Note:</b> if the <tt>maximum request size</tt> or <tt>maximum file size</tt> * is exceeded, the request is deemed invalid ({@link #hasPostError hasPostError} * will return true), and no further processing is performed on the form or fields. * Instead the form will display the appropriate error message for the invalid request. * See {@link #validateFileUpload()} for details of the error message properties. * <p/> * <b>JavaScript Validation</b> * <p/> * The Form control also supports client side JavaScript validation. By default * JavaScript validation is not enabled. To enable JavaScript validation set * {@link #setJavaScriptValidation(boolean)} to true. For example: * * <pre class="prettyprint"> * Form form = new Form("form"); * form.setJavaScriptValidation(true); * * // Add form fields * .. * * form.add(new Submit("ok", " OK ", this, "onOkClicked"); * * Submit cancel = new Submit("cancel", "Cancel", this, "onCancelClicked"); * cancel.setCancelJavaScriptValidation(true); * * addControl(form); </pre> * * Please note in that is this example the cancel submit button has * {@link Submit#setCancelJavaScriptValidation(boolean)} set to true. This * prevents JavaScript form validation being performed when the cancel button is * clicked. * * <a name="resources"></a> * <h3>CSS and JavaScript resources</h3> * * The Form control makes use of the following resources (which Click automatically * deploys to the application directory, <tt>/click</tt>): * * <ul> * <li><tt>click/control.css</tt></li> * <li><tt>click/control.js</tt></li> * </ul> * * To import these files and any form control imports simply reference * the variables <span class="blue">$headElements</span> and * <span class="blue">$jsElements</span> in the page template. For example: * * <pre class="codeHtml"> * <html> * <head> * <span class="blue">$headElements</span> * </head> * <body> * * <span class="red">$form</span> * * <span class="blue">$jsElements</span> * </body> * </html> </pre> * * <a name="form-layout"></a> * <h3>Form Layout</h3> * The Form control supports rendering using automatic and manual layout * techniques. * * <a name="auto-layout"></a> * <h4>Auto Layout</h4> * * If you include a form variable in your template the form will be * automatically laid out and rendered. Auto layout, form and field rendering * options include: * * <table style="margin-left: 1em;" cellpadding="3"> * <tr> * <td>{@link #buttonAlign}</td> <td>button alignment: <tt>["left", "center", "right"]</tt></td> * </tr><tr> * <td>{@link #buttonStyle}</td> <td>button <td> "style" attribute value</td> * </tr><tr> * <td>{@link #columns}</td> <td>number of form table columns, the default value number is 1</td> * </tr><tr> * <td>{@link #errorsAlign}</td> <td>validation error messages alignment: <tt>["left", "center", "right"]</tt></td> * </tr><tr> * <td>{@link #errorsPosition}</td> <td>validation error messages position: <tt>["top", "middle", "bottom"]</tt></td> * </tr><tr> * <td>{@link #errorsStyle}</td> <td>errors <td> "style" attribute value</td> * </tr><tr> * <td>{@link #fieldStyle}</td> <td>field <td> "style" attribute value</td> * </tr><tr> * <td>{@link #labelAlign}</td> <td>field label alignment: <tt>["left", "center", "right"]</tt></td> * </tr><tr> * <td>{@link #labelsPosition}</td> <td>label position relative to field: <tt>["left", "top"]</tt></td> * </tr><tr> * <td>{@link #labelStyle}</td> <td>label <td> "style" attribute value</td> * </tr><tr> * <td>click/control.css</td> <td>control CSS styles, automatically deployed to the <tt>click</tt> web directory</td> * </tr><tr> * <td>/click-control.properties</td> <td>form and field messages and HTML, located under classpath</td> * </tr> * </table> * * <a name="manual-layout"></a> * <h4>Manual Layout</h4> * * You can also manually layout the Form in the page template specifying * the fields using the named field notation: * * <pre class="codeHtml"> * $form.{@link #getFields fields}.usernameField </pre> * * Whenever including your own Form markup in a page template or Velocity macro * always specify: * <ul style="margin-top: 0.5em;"> * <li><span class="maroon">method</span> * - the form submission method <tt>["post" | "get"]</tt></li> * <li><span class="maroon">name</span> * - the name of your form, important when using JavaScript</li> * <li><span class="maroon">action</span> * - directs the Page where the form should be submitted to</li> * <li><span class="maroon">form_name</span> * - include a hidden field which specifies the {@link #name} of the Form </li> * </ul> * The hidden field is used by Click to determine which form was posted on * a page which may contain multiple forms. * <p/> * Alternatively you can use the Form {@link #startTag()} and {@link #endTag()} * methods to render this information. * <p/> * An example of a manually laid out Login form is provided below: * * <pre class="codeHtml"> * <span class="blue">$form.startTag()</span> * * <table style="margin: 1em;"> * * <span class="red">#if</span> (<span class="blue">$form.error</span>) * <tr> * <td colspan="2" style="color: red;"> <span class="blue">$form.error</span> </td> * </tr> * <span class="red">#end</span> * <span class="red">#if</span> (<span class="blue">$form.fields.usernameField.error</span>) * <tr> * <td colspan="2" style="color: red;"> <span class="blue">$form.fields.usernameField.error</span> </td> * </tr> * <span class="red">#end</span> * <span class="red">#if</span> (<span class="blue">$form.fields.passwordField.error</span>) * <tr> * <td colspan="2" style="color: red;"> <span class="blue">$form.fields.passwordField.error</span> </td> * </tr> * <span class="red">#end</span> * * <tr> * <td> Username: </td> * <td> <span class="blue">$form.fields.usernameField</span> </td> * </tr> * <tr> * <td> Password: </td> * <td> <span class="blue">$form.fields.passwordField</span> </td> * </tr> * * <tr> * <td> * <span class="blue">$form.fields.okSubmit</span> * <span class="blue">$form.fields.cancelSubmit</span> * </td> * </tr> * * </table> * * <span class="blue">$form.endTag()</span> </pre> * * As you can see in this example most of the code and markup is generic and * could be reused. This is where Velocity Macros come in. * * <a name="velocity-macros"></a> * <h4>Velocity Macros</h4> * * Velocity Macros * (<a target="topic" href="../../../../../velocity/user-guide.html#Velocimacros">velocimacros</a>) * are a great way to encapsulate customized forms. * <p/> * To create a generic form layout you can use the Form {@link #getFieldList()} and * {@link #getButtonList()} properties within a Velocity macro. If you want to * access <em>all</em> Form Controls from within a Velocity template or macro use * {@link #getControls()}. * <p/> * The example below provides a generic <span class="green">writeForm()</span> * macro which you could use through out an application. This Velocity macro code * would be contained in a macro file, e.g. <tt>macro.vm</tt>. * * <pre class="codeHtml"> <span class="red">#*</span> Custom Form Macro Code <span class="red">*#</span> * <span class="red">#macro</span>( <span class="green">writeForm</span>[<span class="blue">$form</span>] ) * * <span class="blue">$form.startTag()</span> * * <table width="100%"> * * <span class="red">#if</span> (<span class="blue">$form.error</span>) * <tr> * <td colspan="2" style="color: red;"> <span class="blue">$form.error</span> </td> * </tr> * <span class="red">#end</span> * * <span class="red">#foreach</span> (<span class="blue">$field</span> <span class="red">in</span> <span class="blue">$form.fieldList</span>) * <span class="red">#if</span> (!<span class="blue">$field.hidden</span>) * <span class="red">#if</span> (!<span class="blue">$field.valid</span>) * <tr> * <td colspan="2"> <span class="blue">$field.error</span> </td> * </tr> * <span class="red">#end</span> * * <tr> * <td> <span class="blue">$field.label</span>: </td><td> <span class="blue">$field</span> </td> * </tr> * <span class="red">#end</span> * <span class="red">#end</span> * * <tr> * <td colspan="2"> * <span class="red">#foreach</span> (<span class="blue">$button</span> <span class="red">in </span><span class="blue">$form.buttonList</span>) * <span class="blue">$button</span> &nbsp; * <span class="red">#end</span> * </td> * </tr> * * </table> * * <span class="blue">$form.endTag()</span> * * <span class="red">#end</span> </pre> * * You would then call this macro in your Page template passing it your * <span class="blue">form</span> object: * * <pre class="codeHtml"> <span class="red">#</span><span class="green">writeForm</span>(<span class="blue">$form</span>) </pre> * * At render time Velocity will execute the macro using the given form and render * the results to the response output stream. * * <h4>Configuring Macros</h4> * * To configure your application to use your macros you can: * <ul> * <li> * Put your macros if a file called <span class="st"><tt>macro.vm</tt></span> * in your applications root directory. * </li> * <li> * Put your macros in the auto deployed * <span class="st"><tt>click/VM_global_macro.vm</tt></span> file. * </li> * <li> * Create a custom named macro file and reference it in a * <span class="st"><tt>WEB-INF/velocity.properties</tt></span> * file under the property named * <tt>velocimacro.library</tt>. * </li> * </ul> * * <a name="post-redirect"></a> * <h3>Preventing Accidental Form Posts</h3> * * Users may accidentally make multiple form submissions by refreshing a page * or by pressing the back button. * <p/> * To prevent multiple form posts from page refreshes use the Post * Redirect pattern. With this pattern once the user has posted a form you * redirect to another page. If the user then presses the refresh button, they * will making a GET request on the current page. Please see the * <a target="blank" href="http://www.theserverside.com/articles/content/RedirectAfterPost/article.html">Redirect After Post</a> * article for more information on this topic. * <p/> * To prevent multiple form posts from use of the browser back button use one * of the Form {@link #onSubmitCheck(org.apache.click.Page, String)} methods. For example: * * <pre class="prettyprint"> * public class Purchase extends Page { * .. * * public boolean onSecurityCheck() { * return form.onSubmitCheck(this, "/invalid-submit.html"); * } * } </pre> * * The form submit check methods store a special token in the users session * and in a hidden field in the form to ensure a form post isn't replayed. * * <a name="dynamic-forms"></a> * <h3>Dynamic Forms and <em>not</em> validating a request</h3> * * A common use case for web applications is to create Form fields dynamically * based upon user selection. For example if a checkbox is ticked another Field * is added to the Form. A simple way to achieve this is using JavaScript * to submit the Form when the Field is changed or clicked. * <p/> * When submitting a Form using JavaScript, it is often desirable to <em>not</em> * validate the fields since the user is still filling out the form. * To cater for this use case, Form provides the {@link #setValidate(boolean)} * to switch off form and field validation. For example: * * <pre class="prettyprint"> * public void onInit() { * checkbox.setAttribute("onclick", "form.submit()"); * * // Since onInit occurs before the onProcess event, * // we have to explicitly bind the submit button in the onInit event if we * // want to check if it was clicked. * // If the submit button wasn't clicked it means the Form was submitted * // using JavaScript and we don't want to validate yet * ClickUtils.bind(submit); * * // If submit was not clicked, don't validate * if(form.isFormSubmission() && !submit.isClicked()) { * form.setValidate(false); * } * } </pre> * * <p> <p/> * See also the W3C HTML reference: * <a class="external" target="_blank" title="W3C HTML 4.01 Specification" * href="http://www.w3.org/TR/html401/interact/forms.html#h-17.3">FORM</a> * * @see Field * @see Submit */ public class Form extends AbstractContainer implements Stateful { // Constants -------------------------------------------------------------- private static final long serialVersionUID = 1L; /** The align left, form layout constant: <tt>"left"</tt>. */ public static final String ALIGN_LEFT = "left"; /** The align center, form layout constant: <tt>"center"</tt>. */ public static final String ALIGN_CENTER = "center"; /** The align right, form layout constant: <tt>"right"</tt>. */ public static final String ALIGN_RIGHT = "right"; /** * The position top, errors and labels form layout constant: * <tt>"top"</tt>. */ public static final String POSITION_TOP = "top"; /** * The position middle, errors in middle form layout constant: * <tt>"middle"</tt>. */ public static final String POSITION_MIDDLE = "middle"; /** * The position bottom, errors on bottom form layout constant: * <tt>"top"</tt>. */ public static final String POSITION_BOTTOM = "bottom"; /** * The position left, labels of left form layout constant: * <tt>"left"</tt>. */ public static final String POSITION_LEFT = "left"; /** * The form name parameter for multiple forms: <tt>"form_name"</tt>. */ public static final String FORM_NAME = "form_name"; /** The HTTP content type header for multipart forms. */ public static final String MULTIPART_FORM_DATA = "multipart/form-data"; /** * The submit check reserved request parameter prefix: * <tt>SUBMIT_CHECK_</tt>. */ public static final String SUBMIT_CHECK = "SUBMIT_CHECK_"; /** The Form set field focus JavaScript. */ protected static final String FOCUS_JAVASCRIPT = "<script type=\"text/javascript\"><!--\n" + "var field = document.getElementById('$id');\n" + "if (field && field.focus && field.type != 'hidden' && field.disabled != true) { field.focus(); };\n" + "//--></script>\n"; // Instance Variables ----------------------------------------------------- /** The form action URL. */ protected String actionURL; /** The form disabled value. */ protected boolean disabled; /** The form "enctype" attribute. */ protected String enctype; /** The form level error message. */ protected String error; /** The ordered list of fields, excluding buttons. */ protected final List<Field> fieldList = new ArrayList<Field>(); /** * The form method <tt>["post, "get"]</tt>, default value: * <tt>post</tt>. */ protected String method = "post"; /** The form is readonly flag. */ protected boolean readonly; /** The form validate fields when processing flag. */ protected boolean validate = true; /** The button align, default value is "<tt>left</tt>". */ protected String buttonAlign = ALIGN_LEFT; /** The ordered list of button values. */ protected final List<Button> buttonList = new ArrayList<Button>(5); /** The button <td> "style" attribute value. */ protected String buttonStyle; /** * The number of form layout table columns, default value: <tt>1</tt>. * <p/> * This property is used to layout the number of table columns the form * is rendered with using a flow layout style. */ protected int columns = 1; /** * The default field size, default value: <tt>0</tt>. * <p/> * If the form default field size is greater than 0, when fields are added * to the form the field's size will be set to the default value. */ protected int defaultFieldSize; /** The errors block align, default value is <tt>"left"</tt>. */ protected String errorsAlign = ALIGN_LEFT; /** * The form errors position <tt>["top", "middle", "bottom"]</tt> default * value: <tt>"top"</tt>. */ protected String errorsPosition = POSITION_TOP; /** The error <td> "style" attribute value. */ protected String errorsStyle; /** The field <td> "style" attribute value. */ protected String fieldStyle; /** The map of field width values. */ protected Map<String, Integer> fieldWidths = new HashMap<String, Integer>(); /** Flag indicating whether this form was submitted. */ protected Boolean formSubmission; /** * The JavaScript client side form fields validation flag. By default * JavaScript validation is not enabled. */ protected boolean javaScriptValidation; /** The label align, default value is <tt>"left"</tt>. */ protected String labelAlign = ALIGN_LEFT; /** * The form labels position <tt>["left", "top"]</tt> default value: * <tt>"left"</tt>. */ protected String labelsPosition = POSITION_LEFT; /** The label <td> "style" attribute value. */ protected String labelStyle; /** * Track the index offset when adding Controls. This ensures HiddenFields * added by Form does not interfere with Controls added by users. */ private int insertIndexOffset; // Ensures hiddenFields added by Form are always at the end of the controlList // Constructors ----------------------------------------------------------- /** * Create a form with the given name. * * @param name the name of the form * @throws IllegalArgumentException if the form name is null */ public Form(String name) { setName(name); } /** * Create a form with no name. * <p/> * <b>Please note</b> the control's name must be defined before it is valid. */ public Form() { } // Container Impl --------------------------------------------------------- /** * Add the control to the form at the specified index, and return the * added instance. * <p/> * <b>Please note</b>: if the form contains a control with the same name as * the given control, that control will be * {@link #replace(org.apache.click.Control, org.apache.click.Control) replaced} * by the given control. If a control has no name defined it cannot be replaced. * <p/> * Controls can be retrieved from the Map {@link #getControlMap() controlMap} * where the key is the Control name and value is the Control instance. * <p/> * All controls are available on the {@link #getControls() controls} list * at the index they were inserted. If you are only interested in Fields, * note that Buttons are available on the {@link #getButtonList() buttonList} * while other fields are available on {@link #getFieldList() fieldList}. * <p/> * The specified index only applies to {@link #getControls() controls}, not * {@link #getButtonList() buttonList} or {@link #getFieldList() fieldList}. * <p/> * <b>Please note</b> if the specified control already has a parent assigned, * it will automatically be removed from that parent and inserted into the * form. * * @see Container#insert(org.apache.click.Control, int) * * @param control the control to add to the container * @param index the index at which the control is to be inserted * @return the control that was added to the container * * @throws IllegalArgumentException if the control is null or if the control * and container is the same instance or if the Field name is not defined * * @throws IndexOutOfBoundsException if index is out of range * <tt>(index < 0 || index > getControls().size())</tt> */ @Override public Control insert(Control control, int index) { // Check if container already contains the control String controlName = control.getName(); if (controlName != null) { Control currentControl = getControlMap().get(controlName); // If container already contains the control do a replace if (currentControl != null && !(control instanceof Label)) { // Current control and new control are referencing the same object // so we exit early if (currentControl == control) { return control; } // If the two controls are different objects, replace the current // control with the given control return replace(currentControl, control); } } // Adjust index for hidden fields added by Form. CLK-447 int realIndex = Math.min(index, getControls().size() - insertIndexOffset); ContainerUtils.insert(this, control, realIndex, getControlMap()); if (control instanceof Field) { Field field = (Field) control; // Add field to either buttonList or fieldList for fast access if (field instanceof Button) { getButtonList().add((Button) field); } else { // Adjust index for hidden fields added by Form realIndex = Math.min(index, getFieldList().size() - insertIndexOffset); getFieldList().add(realIndex, field); } field.setForm(this); if (getDefaultFieldSize() > 0) { if (field instanceof TextField) { ((TextField) field).setSize(getDefaultFieldSize()); } else if (field instanceof FileField) { ((FileField) field).setSize(getDefaultFieldSize()); } else if (field instanceof TextArea) { ((TextArea) field).setCols(getDefaultFieldSize()); } } } return control; } /** * Add a Control to the form and return the added instance. * <p/> * <b>Please note</b>: if the form contains a control with the same name as * the given control, that control will be * {@link #replace(org.apache.click.Control, org.apache.click.Control) replaced} * by the given control. If a control has no name defined it cannot be replaced. * <p/> * Controls can be retrieved from the Map {@link #getControlMap() controlMap} * where the key is the Control name and value is the Control instance. * <p/> * All controls are available on the {@link #getControls() controls} list * in the order they were added. If you are only interested in Fields, * note that Buttons are available on the {@link #getButtonList() buttonList} * while other fields are available on {@link #getFieldList() fieldList}. * * @see Container#add(org.apache.click.Control) * * @param control the control to add to the container and return * @return the control that was added to the container * @throws IllegalArgumentException if the control is null, the Control name * is not defined or the container already contains a control with the same * name */ @Override public Control add(Control control) { return super.add(control); } /** * Add the field to the form, and set the fields form property. * <p/> * <b>Please note</b>: if the form contains a control with the same name as * the given control, that control will be * {@link #replace(org.apache.click.Control, org.apache.click.Control) replaced} * by the given control. If a control has no name defined it cannot be replaced. * <p/> * Fields can be retrieved from the Map {@link #getFields() fields} where * the key is the Field name and value is the Field instance. * <p/> * Buttons are available on the {@link #getButtonList() buttonList} while * other fields are available on {@link #getFieldList() fieldList}. * * @see #add(org.apache.click.Control) * * @param field the field to add to the form * @return the field added to this form * @throws IllegalArgumentException if the field is null, the field name * is not defined or the form already contains a control with the same name */ public Field add(Field field) { add((Control) field); return field; } /** * Add the field to the form and specify the field's width in columns. * <p/> * <b>Please note</b>: if the form contains a control with the same name as * the given control, that control will be * {@link #replace(org.apache.click.Control, org.apache.click.Control) replaced} * by the given control. If a control has no name defined it cannot be replaced. * <p/> * Fields can be retrieved from the Map {@link #getFields() fields} where * the key is the Field name and value is the Field instance. * <p/> * Fields are available on {@link #getFieldList() fieldList}. * <p/> * Note Button and HiddenField types are not valid arguments for this method. * * @see #add(org.apache.click.Control) * * @param field the field to add to the form * @param width the width of the field in table columns * @return the field added to this form * @throws IllegalArgumentException if the field is null, field name is * not defined, field is a Button or HiddenField, the form already contains * a field with the same name or the width < 1 */ public Field add(Field field, int width) { add((Control) field, width); return field; } /** * Add the control to the form and specify the control's width in columns. * <p/> * <b>Please note</b>: if the form contains a control with the same name as * the given control, that control will be * {@link #replace(org.apache.click.Control, org.apache.click.Control) replaced} * by the given control. If a control has no name defined it cannot be replaced. * <p/> * Controls can be retrieved from the Map {@link #getControlMap() controlMap} * where the key is the Control name and value is the Control instance. * <p/> * Controls are available on the {@link #getControls() controls} list. * <p/> * Note Button and HiddenField types are not valid arguments for this method. * * @see #add(org.apache.click.Control) * * @param control the control to add to the form * @param width the width of the control in table columns * @return the control added to this form * @throws IllegalArgumentException if the control is null, control is a * Button or HiddenField, the form already contains a control with the same * name or the width < 1 */ public Control add(Control control, int width) { if (control instanceof Button || control instanceof HiddenField) { String msg = "Not a valid field type: " + control.getClass().getName(); throw new IllegalArgumentException(msg); } if (width < 1) { throw new IllegalArgumentException("Invalid field width: " + width); } add(control); if (control.getName() != null) { getFieldWidths().put(control.getName(), width); } return control; } /** * Replace the control in the form at the specified index, and return * the newly added control. * * @see org.apache.click.control.Container#replace(org.apache.click.Control, org.apache.click.Control) * * @param currentControl the control currently contained in the form * @param newControl the control to replace the current control contained in * the form * @return the new control that replaced the current control * * @deprecated this method was used for stateful pages, which have been deprecated * * @throws IllegalArgumentException if the currentControl or newControl is * null * @throws IllegalStateException if the currentControl is not contained in * the form */ @Override public Control replace(Control currentControl, Control newControl) { // Current and new control is the same instance - exit early if (currentControl == newControl) { return newControl; } int controlIndex = getControls().indexOf(currentControl); Control result = ContainerUtils.replace(this, currentControl, newControl, controlIndex, getControlMap()); if (newControl instanceof Field) { Field field = (Field) newControl; if (field instanceof Button) { // Replace field in buttonList for fast access int buttonIndex = getButtonList().indexOf(currentControl); getButtonList().set(buttonIndex, (Button) field); } else { // Replace field in fieldList for fast access int fieldIndex = getFieldList().indexOf(currentControl); getFieldList().set(fieldIndex, field); } // Set parent form field.setForm(this); if (currentControl instanceof Field) { // Remove form reference from current control ((Field) currentControl).setForm(null); } if (getDefaultFieldSize() > 0) { if (field instanceof TextField) { ((TextField) field).setSize(getDefaultFieldSize()); } else if (field instanceof FileField) { ((FileField) field).setSize(getDefaultFieldSize()); } else if (field instanceof TextArea) { ((TextArea) field).setCols(getDefaultFieldSize()); } } } return result; } /** * @see Container#remove(org.apache.click.Control) * * @param control the control to remove from the container * @return true if the control was removed from the container * @throws IllegalArgumentException if the control is null */ @Override public boolean remove(Control control) { boolean removed = super.remove(control); if (removed && control instanceof Field) { Field field = (Field) control; field.setForm(null); if (field instanceof Button) { getButtonList().remove(field); } else { getFieldList().remove(field); } } getFieldWidths().remove(control.getName()); return removed; } /** * Remove the named field from the form, returning true if removed * or false if not found. * * @param name the name of the field to remove from the form * @return true if the named field was removed or false otherwise */ public boolean removeField(String name) { Control control = getControl(name); if (control != null) { return remove(control); } else { return false; } } /** * Remove the list of named fields from the form. * * @param fieldNames the list of field names to remove from the form * @throws IllegalArgumentException if any of the fields is null */ public void removeFields(List<String> fieldNames) { if (fieldNames != null) { for (int i = 0; i < fieldNames.size(); i++) { removeField(fieldNames.get(i).toString()); } } } // Public Attributes ------------------------------------------------------ /** * Return the form's html tag: <tt>form</tt>. * * @see AbstractControl#getTag() * * @return this controls html tag */ @Override public String getTag() { return "form"; } /** * Return the form "action" attribute URL value. If the action URL attribute * has not been explicitly set the form action attribute will target the * page containing the form. This is the default behaviour for most scenarios. * However if you explicitly specify the form "action" URL attribute, this * value will be used instead. * <p/> * Setting the form action attribute is useful for situations where you want * a form to submit to a different page. This can also be used to have a * form submit to the J2EE Container for authentication, by setting the * action URL to "<tt>j_security_check</tt>". * <p/> * The action URL will always be encoded by the response to ensure it includes * the Session ID if required. * * @return the form "action" attribute URL value. */ public String getActionURL() { Context context = getContext(); HttpServletResponse response = context.getResponse(); if (actionURL == null) { HttpServletRequest request = context.getRequest(); return response.encodeURL(ClickUtils.getRequestURI(request)); } else { return response.encodeURL(actionURL); } } /** * Return the form "action" attribute URL value. By setting this value you * will override the default action URL which points to the page containing * the form. * <p/> * Setting the form action attribute is useful for situations where you want * a form to submit to a different page. This can also be used to have a * form submit to the J2EE Container for authentication, by setting the * action URL to "<tt>j_security_check</tt>". * * @param value the form "action" attribute URL value */ public void setActionURL(String value) { this.actionURL = value; } /** * @see AbstractControl#getControlSizeEst() * * @return the estimated rendered control size in characters */ @Override public int getControlSizeEst() { return 400 + (getControls().size() * 350); } /** * Return true if the form is a disabled. * * @return true if the form is a disabled */ public boolean isDisabled() { return disabled; } /** * Set the form disabled flag. * * @param disabled the form disabled flag */ public void setDisabled(boolean disabled) { this.disabled = disabled; } /** * Return the form "enctype" attribute value, or null if not defined. * * @return the form "enctype" attribute value, or null if not defined */ public String getEnctype() { if (enctype == null) { for (Field field : ContainerUtils.getInputFields(this)) { if (!field.isHidden() && (field instanceof FileField)) { enctype = MULTIPART_FORM_DATA; break; } } } return enctype; } /** * Set the form "enctype" attribute value. * * @param enctype the form "enctype" attribute value, or null if not defined */ public void setEnctype(String enctype) { this.enctype = enctype; } /** * Return the form level error message. * * @return the form level error message */ public String getError() { return error; } /** * Set the form level validation error message. If the error message is not * null the form is invalid. * * @param error the validation error message */ public void setError(String error) { this.error = error; } /** * Return a list of form fields which are not valid, not hidden and not * disabled. * * @return list of form fields which are not valid, not hidden and not * disabled */ public List<Field> getErrorFields() { return ContainerUtils.getErrorFields(this); } /** * Return the named field if contained in the form or null if not found. * * @param name the name of the field * @return the named field if contained in the form * * @throws IllegalStateException if a non-field control is found with the * specified name */ public Field getField(String name) { Control control = ContainerUtils.findControlByName(this, name); if (control != null && !(control instanceof Field)) { throw new IllegalStateException("The control named " + name + " is an instance of the class " + control.getClass().getName() + ", which is not a " + Field.class.getName() + " subclass."); } return (Field) control; } /** * Return the field value for the named field, or null if the field is not * found. * * @param name the name of the field * @return the field value for the named field */ public String getFieldValue(String name) { Field field = getField(name); if (field != null) { return field.getValue(); } else { return null; } } /** * Return the Form HEAD elements to be included in the page. * The following resources are returned: * * <ul> * <li><tt>click/control.css</tt></li> * <li><tt>click/control.js</tt></li> * </ul> * * @see org.apache.click.Control#getHeadElements() * * @return the form list of HEAD elements to be included in the page */ @Override public List<Element> getHeadElements() { if (headElements == null) { headElements = super.getHeadElements(); Context context = getContext(); String versionIndicator = ClickUtils.getResourceVersionIndicator(context); headElements.add(new CssImport("/click/control.css", versionIndicator)); headElements.add(new JsImport("/click/control.js", versionIndicator)); } return headElements; } /** * Return the form method <tt>["post" | "get"]</tt>, default value is * <tt>post</tt>. * * @return the form method */ public String getMethod() { return method; } /** * Set the form method <tt>["post" | "get"]</tt>. * * @param value the form method */ public void setMethod(String value) { method = value; } /** * Return true if the page request is a submission from this form. * <p/> * A form submission requires the following criteria: * <ul> * <li>the Form name must be present as a request parameter (Form * automatically adds a HiddenField which value is set to the Form name. * This ensures the Form name is present when submitting the form)</li> * <li>the request method must equal the Form {@link #method}, for example * both must be <tt>GET</tt> or <tt>POST</tt></li> * </ul> * * @return true if the page request is a submission from this form */ public boolean isFormSubmission() { if (formSubmission == null) { Context context = getContext(); String requestMethod = context.getRequest().getMethod(); if (!getMethod().equalsIgnoreCase(requestMethod)) { return false; } formSubmission = getName().equals(context.getRequestParameter(FORM_NAME)); } return formSubmission; } /** * Set the name of the form. * * @see org.apache.click.Control#setName(String) * * @param name of the control * @throws IllegalArgumentException if the name is null */ @Override public void setName(String name) { if (name == null) { throw new IllegalArgumentException("Null name parameter"); } this.name = name; // TODO: Remove with stateful pages HiddenField nameField = (HiddenField) getField(FORM_NAME); if (nameField == null) { // Create a hidden field that won't be processed and name cannot change nameField = new NonProcessedHiddenField(FORM_NAME, String.class); add(nameField); insertIndexOffset++; } nameField.setValue(name); } /** * Return true if the form is a readonly. * * @return true if the form is a readonly */ public boolean isReadonly() { return readonly; } /** * Set the form readonly flag. * * @param readonly the form readonly flag */ public void setReadonly(boolean readonly) { this.readonly = readonly; } /** * Return true if the fields are valid and there is no form level error, * otherwise return false. * * @return true if the fields are valid and there is no form level error */ public boolean isValid() { if (getError() != null) { return false; } for (Field field : ContainerUtils.getInputFields(this)) { if (!field.isValid()) { return false; } } return true; } /** * Return the ordered list of form fields, excluding buttons. * <p/> * The order of the fields is the same order they were added to the form. * <p/> * The returned list includes only fields added directly to the Form. * * @return the ordered List of form fields, excluding buttons */ public List<Field> getFieldList() { return fieldList; } /** * Return the Map of form fields (including buttons), keyed * on field name. * <p/> * The returned map includes only fields added directly to the Form. * * @see #getControlMap() * * @return the Map of form fields (including buttons), keyed * on field name */ public Map<String, Control> getFields() { return getControlMap(); } /** * Return true if the Form fields should validate themselves when being * processed. * * @return true if the form fields should perform validation when being * processed */ public boolean getValidate() { return validate; } /** * Set the Form field validation flag, telling the Fields to validate * themselves when their <tt>onProcess()</tt> method is invoked. * * @param validate the Form field validation flag */ public void setValidate(boolean validate) { this.validate = validate; } /** * Return the buttons <td> HTML horizontal alignment: "<tt>left</tt>", * "<tt>center</tt>", "<tt>right</tt>". * * @return the field label HTML horizontal alignment */ public String getButtonAlign() { return buttonAlign; } /** * Set the button <td> HTML horizontal alignment: "<tt>left</tt>", * "<tt>center</tt>", "<tt>right</tt>". * Note the given align is not validated. * * @param align the field label HTML horizontal alignment */ public void setButtonAlign(String align) { buttonAlign = align; } /** * Return the ordered list of {@link Button}s. * <p/> * The order of the buttons is the same order they were added to the form. * <p/> * The returned list includes only buttons added directly to the Form. * * @return the ordered list of {@link Button}s. */ public List<Button> getButtonList() { return buttonList; } /** * Return the button <td> "style" attribute value. * * @return the button <td> "style" attribute value */ public String getButtonStyle() { return buttonStyle; } /** * Set the button <td> "style" attribute value. * * @param value the button <td> "style" attribute value */ public void setButtonStyle(String value) { this.buttonStyle = value; } /** * Return the number of form layout table columns. This property is used to * layout the number of table columns the form is rendered with. * * @return the number of form layout table columns */ public int getColumns() { return columns; } /** * Set the number of form layout table columns. This property is used to * layout the number of table columns the form is rendered with. * * @param columns the number of form layout table columns */ public void setColumns(int columns) { this.columns = columns; } /** * Return the form default field size. If the form default field size is * greater than 0, when fields are added to the form the field's size will * be set to the default value. * * @return the form default field size */ public int getDefaultFieldSize() { return defaultFieldSize; } /** * Return the form default field size. If the form default field size is * greater than 0, when fields are added to the form the field's size will * be set to the default value. * * @param size the default field size */ public void setDefaultFieldSize(int size) { this.defaultFieldSize = size; } /** * Return the errors block HTML horizontal alignment: "<tt>left</tt>", * "<tt>center</tt>", "<tt>right</tt>". * * @return the errors block HTML horizontal alignment */ public String getErrorsAlign() { return errorsAlign; } /** * Set the errors block HTML horizontal alignment: "<tt>left</tt>", * "<tt>center</tt>", "<tt>right</tt>". * Note the given align is not validated. * * @param align the errors block HTML horizontal alignment */ public void setErrorsAlign(String align) { errorsAlign = align; } /** * Return the form errors position <tt>["top", "middle", "bottom"]</tt>. * * @return form errors position */ public String getErrorsPosition() { return errorsPosition; } /** * Set the form errors position <tt>["top", "middle", "bottom"]</tt>. * * @param position the form errors position */ public void setErrorsPosition(String position) { if (POSITION_TOP.equals(position) || POSITION_MIDDLE.equals(position) || POSITION_BOTTOM.equals(position)) { errorsPosition = position; } else { throw new IllegalArgumentException("Invalid position: " + position); } } /** * Return the error <td> "style" attribute value. * * @return the error <td> "style" attribute value */ public String getErrorsStyle() { return errorsStyle; } /** * Set the errors <td> "style" attribute value. * * @param value the errors <td> "style" attribute value */ public void setErrorsStyle(String value) { this.errorsStyle = value; } /** * Return the field <td> "style" attribute value. * * @return the field <td> "style" attribute value */ public String getFieldStyle() { return fieldStyle; } /** * Set the field <td> "style" attribute value. Fields can override * this value by providing a {@link Field#setParentStyleHint(java.lang.String)}. * * @see Field#setParentStyleHint(java.lang.String) * @see Field#setParentStyleClassHint(java.lang.String) * * @param value the field <td> "style" attribute value */ public void setFieldStyle(String value) { this.fieldStyle = value; } /** * Return the map of field width values, keyed on field name. * * @return the map of field width values, keyed on field name */ public Map<String, Integer> getFieldWidths() { return fieldWidths; } /** * Return true if JavaScript client side form validation is enabled. * * @return true if JavaScript client side form validation is enabled */ public boolean isJavaScriptValidation() { return javaScriptValidation; } /** * Return true if JavaScript client side form validation is enabled. * * @deprecated use {@link #isJavaScriptValidation()} instead * * @return true if JavaScript client side form validation is enabled */ public boolean getJavaScriptValidation() { return javaScriptValidation; } /** * Set the JavaScript client side form validation flag. * * @param validate the JavaScript client side validation flag */ public void setJavaScriptValidation(boolean validate) { javaScriptValidation = validate; } /** * Return the field label HTML horizontal alignment: "<tt>left</tt>", * "<tt>center</tt>", "<tt>right</tt>". * * @return the field label HTML horizontal alignment */ public String getLabelAlign() { return labelAlign; } /** * Set the field label HTML horizontal alignment: "<tt>left</tt>", * "<tt>center</tt>", "<tt>right</tt>". * Note the given align is not validated. * * @param align the field label HTML horizontal alignment */ public void setLabelAlign(String align) { labelAlign = align; } /** * Return the form labels position <tt>["left", "top"]</tt>. * * @return form labels position */ public String getLabelsPosition() { return labelsPosition; } /** * Set the form labels position <tt>["left", "top"]</tt>. * * @param position the form labels position */ public void setLabelsPosition(String position) { if (POSITION_LEFT.equals(position) || POSITION_TOP.equals(position)) { labelsPosition = position; } else { throw new IllegalArgumentException("Invalid position: " + position); } } /** * Return the label <td> "style" attribute value. * * @return the label <td> "style" attribute value */ public String getLabelStyle() { return labelStyle; } /** * Set the label <td> "style" attribute value. * <p/> * This value can be overridden by Fields through their * {@link Field#setParentStyleHint(java.lang.String)} property. * * @param value the label <td> "style" attribute value */ public void setLabelStyle(String value) { this.labelStyle = value; } /** * The callback listener will only be called during processing if the field * value is valid. If the field has validation errors the listener will not * be called. * * @see org.apache.click.Control#setListener(Object, String) * * @param listener the listener object with the named method to invoke * @param method the name of the method to invoke */ @Override public void setListener(Object listener, String method) { super.setListener(listener, method); } // Public Methods --------------------------------------------------------- /** * Clear any form or field errors by setting them to null. */ public void clearErrors() { setError(null); for (Field field : ContainerUtils.getInputFields(this)) { field.setError(null); } } /** * Clear all the form field values setting them to null. */ public void clearValues() { for (Field field : ContainerUtils.getInputFields(this)) { if (!field.getName().equals(FORM_NAME) && (!field.getName().startsWith(SUBMIT_CHECK))) { field.setValue(null); } } } /** * Copy the given object's attributes into the Form's field values. In * other words automatically populate Form's field values with the * given objects attributes. * <p/> * The following example populates the Form field with Customer * attributes: * * <pre class="prettyprint"> * public void onGet() { * Long customerId = .. * Customer customer = CustomerDAO.findByPK(customerId); * form.copyFrom(customer); * } </pre> * * copyForm also supports <tt>java.util.Map</tt> as an argument. * <p/> * By specifying a map, the Form's field values will be populated by * matching key/value pairs. A match occurs when the map's key is equal to * a field's name. * <p/> * The following example populates the Form fields with a map's * key/value pairs: * * <pre class="prettyprint"> * public void onInit() { * form = new Form("form"); * form.add(new TextField("name")); * form.add(new TextField("address.street")); * } * * public void onGet() { * Map map = new HashMap(); * map.put("name", "Steve"); * map.put("address.street", "12 Long street"); * form.copyFrom(map); * } </pre> * * For more information on how Fields and Objects are copied see * {@link org.apache.click.util.ContainerUtils#copyObjectToContainer(java.lang.Object, org.apache.click.control.Container)}. * * @param object the object to obtain attribute values from * @throws IllegalArgumentException if the object parameter is null */ public void copyFrom(Object object) { ContainerUtils.copyObjectToContainer(object, this); } /** * Copy the given object's attributes into the Form's field values. In other * words automatically populate Forms field values with the given objects * attributes. copyFrom also supports <tt>java.util.Map</tt> as an argument. * <p/> * If the debug parameter is true, debugging messages will be * logged. * * @see #copyFrom(java.lang.Object) * * @param object the object to obtain attribute values from * @param debug log debug statements when populating the form * @throws IllegalArgumentException if the object parameter is null */ public void copyFrom(Object object, boolean debug) { ContainerUtils.copyObjectToContainer(object, this); } /** * Copy the Form's field values into the given object's attributes. In * other words automatically populate Object attributes with the Form's * field values. * <p/> * The following example populates the Customer attributes with the * Form's field values: * * <pre class="prettyprint"> * public void onPost() { * if (form.isValid()) { * Customer customer = new Customer(); * form.copyTo(customer); * .. * } * return true; * } </pre> * * copyTo also supports <tt>java.util.Map</tt> as an argument. * <p/> * By specifying a map, the map's key/value pairs are populated from * matching Form field names. A match occurs when a field's name is * equal to a map's key. * <p/> * The following example populates the map with the Form field values: * * <pre class="prettyprint"> * public void onInit() { * form = new Form("form"); * form.add(new TextField("name")); * form.add(new TextField("address.street")); * } * * public void onGet() { * Map map = new HashMap(); * map.put("name", null); * map.put("address.street", null); * form.copyTo(map); * } </pre> * * Note that the map acts as a template to specify which fields to populate * from. * * For more information on how Fields and Objects are copied see * {@link org.apache.click.util.ContainerUtils#copyContainerToObject(org.apache.click.control.Container, java.lang.Object)}. * * @param object the object to populate with field values * @throws IllegalArgumentException if the object parameter is null */ public void copyTo(Object object) { ContainerUtils.copyContainerToObject(this, object); } /** * Copy the Form's field values into the given object's attributes. In other * words automatically populate Object attributes with the Forms field * values. copyTo also supports <tt>java.util.Map</tt> as an argument. * <p/> * If the debug parameter is true, debugging messages will be * logged. * * @see #copyTo(java.lang.Object) * * @param object the object to populate with field values * @param debug log debug statements when populating the object * @throws IllegalArgumentException if the object parameter is null */ public void copyTo(Object object, boolean debug) { ContainerUtils.copyContainerToObject(this, object); } /** * Return the form state. The following state is returned: * * <ul> * <li>all the input Field values and FieldSets contained in the Form and * child containers.</li> * </ul> * * @return the state of input Fields and FieldSets contained in the form */ public Object getState() { List<Field> fields = new ArrayList<Field>(); addStatefulFields(this, fields); Map<String, Object> stateMap = new HashMap<String, Object>(); for (Field field : fields) { Object state = field.getState(); if (state != null) { stateMap.put(field.getName(), state); } } if (stateMap.isEmpty()) { return null; } return stateMap; } /** * Set the Form state. The state will be applied to all the input Fields * and FieldSets contained in the Form or child containers. * * @param state the Form state to set */ public void setState(Object state) { if (state == null) { return; } Map stateMap = (Map) state; List<Field> fields = new ArrayList<Field>(); addStatefulFields(this, fields); for (Field field : fields) { String fieldName = field.getName(); if (stateMap.containsKey(fieldName)) { Object fieldState = stateMap.get(fieldName); field.setState(fieldState); } } } /** * Process the Form and its child controls only if the Form was submitted * by the user. * <p/> * This method invokes {@link #isFormSubmission()} to check whether the form * was submitted or not. * <p/> * The Forms processing order is: * <ol> * <li>All {@link Field} controls in the order they were added</li> * <li>All {@link Button} controls in the order they were added</li> * <li>Invoke the Forms listener if defined</li> * </ol> * * This method delegates validation to {@link #validate()} while * file upload validation are delegated to {@link #validateFileUpload()}. * * @see org.apache.click.Context#getRequestParameter(String) * @see org.apache.click.Context#getFileItemMap() * * @return true to continue Page event processing or false otherwise */ @Override public boolean onProcess() { validateFileUpload(); // If a POST error occurred exit early. if (hasPostError()) { // Remove exception to ensure other forms on Page do not // validate twice for same error. getContext().getRequest().removeAttribute(FileUploadService.UPLOAD_EXCEPTION); return true; } boolean continueProcessing = true; if (isFormSubmission()) { for (int i = 0, size = getControls().size(); i < size; i++) { Control control = getControls().get(i); String controlName = control.getName(); if (controlName == null || !controlName.startsWith(Form.SUBMIT_CHECK)) { if (!control.onProcess()) { continueProcessing = false; } } } if (getValidate()) { validate(); } dispatchActionEvent(); } return continueProcessing; } /** * Destroy the controls contained in the Form and clear any form * error message. * * @see org.apache.click.Control#onDestroy() */ @Override public void onDestroy() { super.onDestroy(); formSubmission = null; setError(null); } /** * The validate method is invoked by {@link #onProcess()} to validate * the request submission. A Form subclass can override this method * to implement cross-field validation logic. * <p/> * If the Form determines that the submission is invalid it should set the * {@link #error} property with an appropriate error message. For example: * * <pre class="prettyprint"> * public class RegistrationForm extends Form { * * // Add validation to ensure the password and confirmPassword fields match * public void validate() { * String password = getFieldValue("password"); * String confirmPassword = getFieldValue("confirmPassword"); * if (!password.equals(confirmPassword)) { * * // Set Form's error property value that will be shown to the user * setError("The passwords do not match."); * } * } * } </pre> */ public void validate() { } /** * Perform a form submission check ensuring the user has not replayed the * form submission by using the browser's back or refresh buttons or by * clicking the Form submit button twice, in quick succession. If the form * submit is valid this method will return true, otherwise set the page to * redirect to the given redirectPath and return false. * <p/> * This method will add a token to the user's session and a hidden field * to the form to validate future submits. * <p/> * Form submit checks should be performed before the pages controls are * processed in the Page onSecurityCheck method. For example: * * <pre class="prettyprint"> * public class Order extends Page { * .. * * public boolean onSecurityCheck() { * return form.onSubmitCheck(this, "/invalid-submit.html"); * } * } </pre> * * Form submit checks should generally be combined with the Post-Redirect * pattern which provides a better user experience when pages are refreshed. * <p/> * <b>Please note:</b> a call to onSubmitCheck always succeeds for Ajax * requests. * * @param page the page invoking the Form submit check * @param redirectPath the path to redirect invalid submissions to * @return true if the form submit is OK or false otherwise * @throws IllegalArgumentException if the page or redirectPath is null */ public boolean onSubmitCheck(Page page, String redirectPath) { if (page == null) { throw new IllegalArgumentException("Null page parameter"); } if (redirectPath == null) { throw new IllegalArgumentException("Null redirectPath parameter"); } if (performSubmitCheck()) { return true; } else { page.setRedirect(redirectPath); return false; } } /** * Perform a form submission check ensuring the user has not replayed the * form submission by using the browser back button. If the form submit * is valid this method will return true, otherwise set the page to * redirect to the given Page class and return false. * <p/> * This method will add a token to the user's session and a hidden field * to the form to validate future submits. * <p/> * Form submit checks should be performed before the pages controls are * processed in the Page onSecurityCheck method. For example: * * <pre class="prettyprint"> * public class Order extends Page { * .. * * public boolean onSecurityCheck() { * return form.onSubmitCheck(this, InvalidSubmitPage.class); * } * } </pre> * * Form submit checks should generally be combined with the Post-Redirect * pattern which provides a better user experience when pages are refreshed. * <p/> * <b>Please note:</b> a call to onSubmitCheck always succeeds for Ajax * requests. * * @param page the page invoking the Form submit check * @param pageClass the page class to redirect invalid submissions to * @return true if the form submit is OK or false otherwise * @throws IllegalArgumentException if the page or pageClass is null */ public boolean onSubmitCheck(Page page, Class<? extends Page> pageClass) { if (page == null) { throw new IllegalArgumentException("Null page parameter"); } if (pageClass == null) { throw new IllegalArgumentException("Null pageClass parameter"); } if (performSubmitCheck()) { return true; } else { page.setRedirect(pageClass); return false; } } /** * Perform a form submission check ensuring the user has not replayed the * form submission by using the browser back button. If the form submit * is valid this method will return true, otherwise the given listener * object and method will be invoked. * <p/> * This method will add a token to the users session and a hidden field * to the form to validate future submit's. * <p/> * Form submit checks should be performed before the pages controls are * processed in the Page onSecurityCheck method. For example: * * <pre class="prettyprint"> * public class Order extends Page { * .. * * public boolean onSecurityCheck() { * return form.onSubmitCheck(his, this, "onInvalidSubmit"); * } * * public boolean onInvalidSubmit() { * getContext().setRequestAttribute("invalidPath", getPath()); * setForward("invalid-submit.htm"); * return false; * } * } </pre> * * Form submit checks should generally be combined with the Post-Redirect * pattern which provides a better user experience when pages are refreshed. * <p/> * <b>Please note:</b> a call to onSubmitCheck always succeeds for Ajax * requests. * * @param page the page invoking the Form submit check * @param submitListener the listener object to call when an invalid submit * occurs * @param submitListenerMethod the listener method to invoke when an * invalid submit occurs * @return true if the form submit is valid, or the return value of the * listener method otherwise * @throws IllegalArgumentException if the page, submitListener or * submitListenerMethod is null */ public boolean onSubmitCheck(Page page, Object submitListener, String submitListenerMethod) { if (page == null) { throw new IllegalArgumentException("Null page parameter"); } if (submitListener == null) { throw new IllegalArgumentException("Null submitListener parameter"); } if (submitListenerMethod == null) { String msg = "Null submitListenerMethod parameter"; throw new IllegalArgumentException(msg); } if (performSubmitCheck()) { return true; } else { return ClickUtils.invokeListener(submitListener, submitListenerMethod); } } /** * Remove the Form state from the session for the given request context. * * @see #saveState(org.apache.click.Context) * @see #restoreState(org.apache.click.Context) * * @param context the request context */ public void removeState(Context context) { ClickUtils.removeState(this, getName(), context); } /** * Restore the Form state from the session for the given request context. * <p/> * This method delegates to {@link #setState(java.lang.Object)} to set the * form restored state. * * @see #saveState(org.apache.click.Context) * @see #removeState(org.apache.click.Context) * * @param context the request context */ public void restoreState(Context context) { ClickUtils.restoreState(this, getName(), context); } /** * Save the Form state to the session for the given request context. * <p/> * * This method delegates to {@link #getState()} to retrieve the form state * to save. * * @see #restoreState(org.apache.click.Context) * @see #removeState(org.apache.click.Context) * * @param context the request context */ public void saveState(Context context) { ClickUtils.saveState(this, getName(), context); } /** * Return the rendered opening form tag and all the forms hidden fields. * * @return the rendered form start tag and the forms hidden fields */ public String startTag() { List<Field> formFields = ContainerUtils.getInputFields(this); int bufferSize = getFormSizeEst(formFields); HtmlStringBuffer buffer = new HtmlStringBuffer(bufferSize); renderHeader(buffer, formFields); return buffer.toString(); } /** * Return the rendered form end tag and JavaScript for field focus * and validation. * * @return the rendered form end tag */ public String endTag() { HtmlStringBuffer buffer = new HtmlStringBuffer(); List<Field> formFields = ContainerUtils.getInputFields(this); renderTagEnd(formFields, buffer); return buffer.toString(); } /** * Render the HTML representation of the Form. * <p/> * If the form contains errors after processing, these errors will be * rendered. * * @see #toString() * * @param buffer the specified buffer to render the control's output to */ @Override public void render(HtmlStringBuffer buffer) { final boolean process = getContext().getRequest().getMethod().equalsIgnoreCase(getMethod()); List<Field> formFields = ContainerUtils.getInputFields(this); renderHeader(buffer, formFields); buffer.append("<table class=\"form\" id=\""); buffer.append(getId()); buffer.append("-form\"><tbody>\n"); // Render fields, errors and buttons if (POSITION_TOP.equals(getErrorsPosition())) { renderErrors(buffer, process); renderFields(buffer); renderButtons(buffer); } else if (POSITION_MIDDLE.equals(getErrorsPosition())) { renderFields(buffer); renderErrors(buffer, process); renderButtons(buffer); } else if (POSITION_BOTTOM.equals(getErrorsPosition())) { renderFields(buffer); renderButtons(buffer); renderErrors(buffer, process); } else { String msg = "Invalid errorsPosition:" + getErrorsPosition(); throw new IllegalArgumentException(msg); } buffer.append("</tbody></table>\n"); renderTagEnd(formFields, buffer); } // Protected Methods ------------------------------------------------------ /** * Perform a back button submit check, returning true if the request is * valid or false otherwise. This method will add a submit check token * to the form as a hidden field, and to the session. * * @return true if the submit is OK or false otherwise */ protected boolean performSubmitCheck() { if (StringUtils.isBlank(getName())) { throw new IllegalStateException("Form name is not defined."); } // CLK-333. Don't regenerate submit tokens for Ajax requests. Context context = getContext(); if (context.isAjaxRequest()) { return true; } String resourcePath = context.getResourcePath(); int slashIndex = resourcePath.indexOf('/'); if (slashIndex != -1) { resourcePath = resourcePath.replace('/', '_'); } // Ensure resourcePath starts with a '_' separator. If slashIndex == -1 // or slashIndex > 0, resourcePath does not start with slash. if (slashIndex != 0) { resourcePath = '_' + resourcePath; } final HttpServletRequest request = context.getRequest(); final String submitTokenName = SUBMIT_CHECK + getName() + resourcePath; boolean isValidSubmit = true; // If not this form exit String formName = context.getRequestParameter(FORM_NAME); // Only test if submit for this form if (!context.isForward() && request.getMethod().equalsIgnoreCase(getMethod()) && getName().equals(formName)) { Long sessionTime = (Long) context.getSessionAttribute(submitTokenName); if (sessionTime != null) { String value = context.getRequestParameter(submitTokenName); if (value == null || value.length() == 0) { // CLK-289. If a session attribute exists for the // SUBMIT_CHECK, but no request parameter, we assume the // submission is a duplicate and therefore invalid. LogService logService = ClickUtils.getLogService(); logService.warn(" 'Redirect After Post' token called '" + submitTokenName + "' is registered in the session, " + "but no matching request parameter was found. " + "(form name: '" + getName() + "'). To protect against a 'duplicate post', " + "Form.onSubmitCheck() will return false."); isValidSubmit = false; } else { Long formTime = Long.valueOf(value); isValidSubmit = formTime.equals(sessionTime); } } } // CLK-267: check against adding a duplicate field HiddenField field = (HiddenField) getField(submitTokenName); if (field == null) { field = new NonProcessedHiddenField(submitTokenName, Long.class); add(field); insertIndexOffset++; } // Save state info to form and session final Long time = System.currentTimeMillis(); field.setValueObject(time); context.setSessionAttribute(submitTokenName, time); if (isValidSubmit) { return true; } else { return false; } } /** * Return the estimated rendered form size in characters. * * @param formFields the list of form fields * @return the estimated rendered form size in characters */ protected int getFormSizeEst(List<Field> formFields) { return 500 + (formFields.size() * 350); } /** * Render the given form start tag and the form hidden fields to the given * buffer. * * @param buffer the HTML string buffer to render to * @param formFields the list of form fields */ protected void renderHeader(HtmlStringBuffer buffer, List<Field> formFields) { buffer.elementStart(getTag()); buffer.appendAttribute("method", getMethod()); buffer.appendAttribute("name", getName()); buffer.appendAttribute("id", getId()); buffer.appendAttribute("action", getActionURL()); buffer.appendAttribute("enctype", getEnctype()); appendAttributes(buffer); if (isJavaScriptValidation()) { String javaScript = "return on_" + getId() + "_submit();"; buffer.appendAttribute("onsubmit", javaScript); } buffer.closeTag(); buffer.append("\n"); // Render hidden fields for (Field field : ContainerUtils.getHiddenFields(this)) { field.render(buffer); buffer.append("\n"); } } /** * Render the non hidden Form Fields to the string buffer. * <p/> * This method delegates the rendering of the form fields to * {@link #renderControls(HtmlStringBuffer, Container, List, Map, int)}. * * @param buffer the StringBuffer to render to */ protected void renderFields(HtmlStringBuffer buffer) { // If Form contains only the FORM_NAME HiddenField, exit early if (getControls().size() == 1) { // getControlMap is cheaper than getFieldMap, so check that first if (getControlMap().containsKey(FORM_NAME)) { return; } else { Map<String, Field> fieldMap = ContainerUtils.getFieldMap(this); if (fieldMap.containsKey(FORM_NAME)) { return; } } } buffer.append("<tr><td>\n"); renderControls(buffer, this, getControls(), getFieldWidths(), getColumns()); buffer.append("</td></tr>\n"); } /** * Render the specified controls of the container to the string buffer. * <p/> * fieldWidths is a map specifying the width for specific fields contained * in the list of controls. The fieldWidths map is keyed on field name. * * @param buffer the StringBuffer to render to * @param container the container which controls to render * @param controls the controls to render * @param fieldWidths a map of field widths keyed on field name * @param columns the number of form layout table columns */ protected void renderControls(HtmlStringBuffer buffer, Container container, List<Control> controls, Map<String, Integer> fieldWidths, int columns) { buffer.append("<table class=\"fields\""); String containerId = container.getId(); if (containerId != null) { buffer.appendAttribute("id", containerId + "-fields"); } buffer.append("><tbody>\n"); int column = 1; boolean openTableRow = false; for (Control control : controls) { // Buttons are rendered separately if (control instanceof Button) { continue; } if (!isHidden(control)) { // Control width Integer width = fieldWidths.get(control.getName()); if (column == 1) { buffer.append("<tr class=\"fields\">\n"); openTableRow = true; } if (control instanceof FieldSet) { FieldSet fieldSet = (FieldSet) control; buffer.append("<td class=\"fields"); String cellStyleClass = fieldSet.getParentStyleClassHint(); if (cellStyleClass != null) { buffer.append(" "); buffer.append(cellStyleClass); } buffer.append("\""); buffer.appendAttribute("style", fieldSet.getParentStyleHint()); if (width != null) { int colspan = (width.intValue() * 2); buffer.appendAttribute("colspan", colspan); } else { buffer.appendAttribute("colspan", 2); } buffer.append(">\n"); control.render(buffer); buffer.append("</td>\n"); } else if (control instanceof Label) { Label label = (Label) control; buffer.append("<td align=\""); buffer.append(getLabelAlign()); buffer.append("\" class=\"fields"); String cellStyleClass = label.getParentStyleClassHint(); if (cellStyleClass != null) { buffer.append(" "); buffer.append(cellStyleClass); } buffer.append("\""); buffer.appendAttribute("style", label.getParentStyleHint()); if (width != null) { int colspan = (width.intValue() * 2); buffer.appendAttribute("colspan", colspan); } else { buffer.appendAttribute("colspan", 2); } if (label.hasAttributes()) { Map<String, String> labelAttributes = label.getAttributes(); for (Map.Entry<String, String> entry : labelAttributes.entrySet()) { String labelAttrName = entry.getKey(); if (!labelAttrName.equals("id") && !labelAttrName.equals("style")) { buffer.appendAttributeEscaped(labelAttrName, entry.getValue()); } } } buffer.append(">"); label.render(buffer); buffer.append("</td>\n"); } else if (control instanceof Field) { Field field = (Field) control; // Write out label if (POSITION_LEFT.equals(getLabelsPosition())) { buffer.append("<td class=\"fields"); String cellStyleClass = field.getParentStyleClassHint(); if (cellStyleClass != null) { buffer.append(" "); buffer.append(cellStyleClass); } buffer.append("\""); buffer.appendAttribute("align", getLabelAlign()); String cellStyle = field.getParentStyleHint(); if (cellStyle == null) { cellStyle = getLabelStyle(); } buffer.appendAttribute("style", cellStyle); buffer.append(">"); } else { buffer.append("<td valign=\"top\" class=\"fields"); String cellStyleClass = field.getParentStyleClassHint(); if (cellStyleClass != null) { buffer.append(" "); buffer.append(cellStyleClass); } buffer.append("\""); String cellStyle = field.getParentStyleHint(); if (cellStyle == null) { cellStyle = getLabelStyle(); } buffer.appendAttribute("style", cellStyle); buffer.append(">"); } // Store the field id and label (the values could be null) String fieldId = field.getId(); String fieldLabel = field.getLabel(); // Only render a label if the fieldId and fieldLabel is set if (fieldId != null && fieldLabel != null) { if (field.isRequired()) { buffer.append(getMessage("label-required-prefix")); } else { buffer.append(getMessage("label-not-required-prefix")); } buffer.elementStart("label"); buffer.appendAttribute("for", fieldId); buffer.appendAttribute("style", field.getLabelStyle()); if (field.isDisabled()) { buffer.appendAttributeDisabled(); } String cellClass = field.getLabelStyleClass(); if (field.getError() == null) { buffer.appendAttribute("class", cellClass); } else { buffer.append(" class=\"error"); if (cellClass != null) { buffer.append(" "); buffer.append(cellClass); } buffer.append("\""); } buffer.closeTag(); buffer.append(fieldLabel); buffer.elementEnd("label"); if (field.isRequired()) { buffer.append(getMessage("label-required-suffix")); } else { buffer.append(getMessage("label-not-required-suffix")); } } if (POSITION_LEFT.equals(getLabelsPosition())) { buffer.append("</td>\n"); buffer.append("<td"); buffer.appendAttribute("class", field.getParentStyleClassHint()); buffer.appendAttribute("align", "left"); String cellStyle = field.getParentStyleHint(); if (cellStyle == null) { cellStyle = getFieldStyle(); } buffer.appendAttribute("style", cellStyle); if (width != null) { int colspan = (width.intValue() * 2) - 1; buffer.appendAttribute("colspan", colspan); } buffer.append(">"); } else { buffer.append("<br/>"); } // Write out field field.render(buffer); buffer.append("</td>\n"); } else { buffer.append("<td class=\"fields\""); if (width != null) { int colspan = (width.intValue() * 2); buffer.appendAttribute("colspan", colspan); } else { buffer.appendAttribute("colspan", 2); } buffer.append(">\n"); control.render(buffer); buffer.append("</td>\n"); } if (width != null) { if (control instanceof Label || !(control instanceof Field)) { column += width.intValue(); } else { column += (width.intValue() - 1); } } if (column >= columns) { buffer.append("</tr>\n"); openTableRow = false; column = 1; } else { column++; } } } if (openTableRow) { buffer.append("</tr>\n"); } buffer.append("</tbody></table>\n"); } /** * Render the form errors to the given buffer is form processed. * * @param buffer the string buffer to render the errors to * @param processed the flag indicating whether has been processed */ protected void renderErrors(HtmlStringBuffer buffer, boolean processed) { if (processed && !isValid()) { buffer.append("<tr><td align=\""); buffer.append(getErrorsAlign()); buffer.append("\">\n"); buffer.append("<table class=\"errors\" id=\""); buffer.append(getId()); buffer.append("-errors\"><tbody>\n"); if (getError() != null) { buffer.append("<tr class=\"errors\">"); buffer.append("<td class=\"errors\""); buffer.appendAttribute("align", getErrorsAlign()); buffer.appendAttribute("colspan", getColumns() * 2); buffer.appendAttribute("style", getErrorsStyle()); buffer.append(">\n"); buffer.append("<span class=\"error\">"); buffer.append(getError()); buffer.append("</span>\n"); buffer.append("</td></tr>\n"); } for (Field field : getErrorFields()) { // Certain fields might be invalid because // one of their contained fields are invalid. However these // fields might not have an error message to display. // If the outer field's error message is null don't render. if (field.getError() == null) { continue; } buffer.append("<tr class=\"errors\">"); buffer.append("<td class=\"errors\""); buffer.appendAttribute("align", getErrorsAlign()); buffer.appendAttribute("colspan", getColumns() * 2); buffer.appendAttribute("style", getErrorsStyle()); buffer.append(">"); buffer.append("<a class=\"error\""); buffer.append(" href=\"javascript:"); buffer.append(field.getFocusJavaScript()); buffer.append("\">"); buffer.append(field.getError()); buffer.append("</a>"); buffer.append("</td></tr>\n"); } buffer.append("</tbody></table>\n"); buffer.append("</td></tr>\n"); } // Render JavaScript form validation code if (isJavaScriptValidation()) { buffer.append("<tr style=\"display:none\" id=\""); buffer.append(getId()); buffer.append("-errorsTr\"><td width='100%' align=\""); buffer.append(getErrorsAlign()); buffer.append("\">\n"); buffer.append("<div class=\"errors\" id=\""); buffer.append(getId()); buffer.append("-errorsDiv\"></div>\n"); buffer.append("</td></tr>\n"); } } /** * Render the given list of Buttons to the string buffer. * * @param buffer the StringBuffer to render to */ protected void renderButtons(HtmlStringBuffer buffer) { List<Button> buttons = getButtonList(); if (!buttons.isEmpty()) { buffer.append("<tr><td"); buffer.appendAttribute("align", getButtonAlign()); buffer.append(">\n"); buffer.append("<table class=\"buttons\" id=\""); buffer.append(getId()); buffer.append("-buttons\"><tbody>\n"); buffer.append("<tr class=\"buttons\">"); for (Button button : buttons) { buffer.append("<td class=\"buttons\""); buffer.appendAttribute("style", getButtonStyle()); buffer.closeTag(); button.render(buffer); buffer.append("</td>"); } buffer.append("</tr>\n"); buffer.append("</tbody></table>\n"); buffer.append("</td></tr>\n"); } } /** * Close the form tag and render any additional content after the Form. * <p/> * Additional content includes <tt>javascript validation</tt> and * <tt>javascript focus</tt> scripts. * * @param formFields all fields contained within the form * @param buffer the buffer to render to */ protected void renderTagEnd(List<Field> formFields, HtmlStringBuffer buffer) { buffer.elementEnd(getTag()); buffer.append("\n"); renderFocusJavaScript(buffer, formFields); renderValidationJavaScript(buffer, formFields); } /** * Render the Form field focus JavaScript to the string buffer. * * @param buffer the StringBuffer to render to * @param formFields the list of form fields */ protected void renderFocusJavaScript(HtmlStringBuffer buffer, List<Field> formFields) { // Set field focus boolean errorFieldFound = false; for (int i = 0, size = formFields.size(); i < size; i++) { Field field = formFields.get(i); if (field.getError() != null && !field.isHidden() && !field.isDisabled()) { String focusJavaScript = StringUtils.replace(FOCUS_JAVASCRIPT, "$id", field.getId()); buffer.append(focusJavaScript); errorFieldFound = true; break; } } if (!errorFieldFound) { for (int i = 0, size = formFields.size(); i < size; i++) { Field field = formFields.get(i); if (field.getFocus() && !field.isHidden() && !field.isDisabled()) { String focusJavaScript = StringUtils.replace(FOCUS_JAVASCRIPT, "$id", field.getId()); buffer.append(focusJavaScript); break; } } } } /** * Render the Form validation JavaScript to the string buffer. * * @param buffer the StringBuffer to render to * @param formFields the list of form fields */ protected void renderValidationJavaScript(HtmlStringBuffer buffer, List<Field> formFields) { // Render JavaScript form validation code if (isJavaScriptValidation()) { List<String> functionNames = new ArrayList<String>(); buffer.append("<script type=\"text/javascript\"><!--\n"); // Render field validation functions & build list of function names for (Field field : formFields) { String fieldJS = field.getValidationJavaScript(); if (fieldJS != null) { buffer.append(fieldJS); StringTokenizer tokenizer = new StringTokenizer(fieldJS); tokenizer.nextToken(); functionNames.add(tokenizer.nextToken()); } } if (!functionNames.isEmpty()) { buffer.append("function on_"); buffer.append(getId()); buffer.append("_submit() {\n"); buffer.append(" var msgs = new Array("); buffer.append(functionNames.size()); buffer.append(");\n"); for (int i = 0; i < functionNames.size(); i++) { buffer.append(" msgs["); buffer.append(i); buffer.append("] = "); buffer.append(functionNames.get(i).toString()); buffer.append(";\n"); } buffer.append(" return validateForm(msgs, '"); buffer.append(getId()); buffer.append("', '"); buffer.append(getErrorsAlign()); buffer.append("', "); if (getErrorsStyle() == null) { buffer.append("null"); } else { buffer.append("'" + getErrorsStyle() + "'"); } buffer.append(");\n"); buffer.append("}\n"); } else { buffer.append("function on_"); buffer.append(getId()); buffer.append("_submit() { return true; }\n"); } buffer.append("//--></script>\n"); } } /** * Returns true if a POST error occurred, false otherwise. * * @return true if a POST error occurred, false otherwise */ protected boolean hasPostError() { Exception e = (Exception) getContext().getRequest().getAttribute(FileUploadService.UPLOAD_EXCEPTION); if (e instanceof FileSizeLimitExceededException || e instanceof SizeLimitExceededException) { return true; } return false; } /** * Validate the request for any file upload (multipart) errors. * <p/> * A form error message is displayed if a file upload error occurs. * These messages are defined in the resource bundle: * <blockquote> * <ul> * <li>/click-control.properties * <ul> * <li>file-size-limit-exceeded-error</li> * <li>post-size-limit-exceeded-error</li> * </ul> * </li> * </ul> * </blockquote> */ protected void validateFileUpload() { setError(null); Exception exception = (Exception) getContext().getRequest() .getAttribute(FileUploadService.UPLOAD_EXCEPTION); if (!(exception instanceof FileUploadException)) { return; } FileUploadException fue = (FileUploadException) exception; String key = null; Object args[] = null; if (fue instanceof SizeLimitExceededException) { SizeLimitExceededException se = (SizeLimitExceededException) fue; key = "post-size-limit-exceeded-error"; args = new Object[2]; args[0] = se.getPermittedSize(); args[1] = se.getActualSize(); setError(getMessage(key, args)); } else if (fue instanceof FileSizeLimitExceededException) { FileSizeLimitExceededException fse = (FileSizeLimitExceededException) fue; key = "file-size-limit-exceeded-error"; // Parse the FileField name from the message String msg = fue.getMessage(); int start = 10; int end = msg.indexOf(' ', start); String fieldName = fue.getMessage().substring(start, end); args = new Object[3]; args[0] = ClickUtils.toLabel(fieldName); args[1] = fse.getPermittedSize(); args[2] = fse.getActualSize(); setError(getMessage(key, args)); } } // Private Methods -------------------------------------------------------- /** * Add fields for the given Container to the specified field list, * recursively including any Fields contained in child containers. * * @param container the container to obtain the fields from * @param fields the list of contained fields */ private void addStatefulFields(final Container container, final List<Field> fields) { for (Control control : container.getControls()) { if (control instanceof Label || control instanceof Button || control instanceof NonProcessedHiddenField) { // Skip buttons and labels and NonProcessedHiddenFields continue; } if (control instanceof Field) { fields.add((Field) control); } else if (control instanceof Container) { Container childContainer = (Container) control; addStatefulFields(childContainer, fields); } } } /** * Return true if the control is hidden, false otherwise. * * @param control control to check hidden status * @return true if the control is hidden, false otherwise */ private boolean isHidden(Control control) { if (!(control instanceof Field)) { // Non-Field Controls can not be hidden return false; } else { return ((Field) control).isHidden(); } } // Inner Classes ---------------------------------------------------------- /** * Provides a HiddenField which does not get processed or bind to its * incoming value. In addition the field name cannot be changed once set. */ private static class NonProcessedHiddenField extends HiddenField { private static final long serialVersionUID = 1L; /** * Create a field with the given name and class. * * @param name the field name * @param valueClass the Class of the value Object */ public NonProcessedHiddenField(String name, Class<?> valueClass) { super(name, valueClass); } /** * Create a field with the given name and value. * * @param name the field name * @param value the value of the field */ public NonProcessedHiddenField(String name, Object value) { super(name, value); } /** * This method is overridden to not change the field name once it is set. * * @param name the name of the field */ @Override public void setName(String name) { if (this.name != null) { return; } super.setName(name); } /** * Overridden to not process the field or bind to its request value. */ @Override public boolean onProcess() { return true; } } /** * Provides a HiddenField which name and value cannot be changed, once it * is set. */ private static class ImmutableHiddenField extends NonProcessedHiddenField { private static final long serialVersionUID = 1L; /** * Create a field with the given name and value. * * @param name the field name * @param value the value of the field */ public ImmutableHiddenField(String name, Object value) { super(name, value); } /** * This method is overridden to not change the field value once it is set. * * @param value the field value */ @Override public void setValue(String value) { if (this.value != null) { return; } super.setValue(value); } /** * This method is overridden to not change the field value object once * it is set. * * @param valueObject the field value object */ @Override public void setValueObject(Object valueObject) { if (this.valueObject != null) { return; } super.setValueObject(valueObject); } } }