/* * Copyright (C) 2014 Civilian Framework. * * Licensed under the Civilian License (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.civilian-framework.org/license.txt * * 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.civilian.form; import java.util.ArrayList; import org.civilian.Request; import org.civilian.Response; import org.civilian.annotation.Get; import org.civilian.annotation.Post; import org.civilian.provider.RequestProvider; import org.civilian.template.HtmlUtil; import org.civilian.template.TemplateWriter; import org.civilian.util.Check; /** * Form represents a HTML form. * The form class helps to implement request-response interactions involving HTML forms.<br> * You build a form by adding controls to the form (@link {@link #add(Control)}).<br> * A form can then be used to print a form in a HTML response (initial presentation to the client). * When the form data is sent back to the server, you instantiate the Form object again, * and let the controls read the submitted values from the request. * Controls are typed, therefore low level extraction of parameter values and conversion * to the target type is handled by the controls. Error information about missing * or invalid form values is also available.<p> * It is good practice to derive own classes from Form for your application forms * and add the controls within the constructor of the implementation class.<br> * Form objects are constructed and used only for a single request. * This is how to implement a typical request-response cycle using forms: * <ol> * <li>In the first (GET)-request to a resource, you would construct the form * and print it to the response. * <li>When the client submits the form, * you detect that the form was submitted (@link #isSubmitted()}. * You now read the values of the controls {@link #read()}. * If the form is invalid (control values are missing, have wrong types, or * fail your custom validation) you would simply print the form again, * which is then presented to the client with his own input and any * additional error information. * <li>If the submitted form is valid, you can extract the control values and process * them. * </ol> * The same resource URL can be used to implement this cycle. To differentiate between * the request phase (initial request or subsequent submits) you can use {@link #isSubmitted()} * and proceed conditionally within your resource method. If POST is used for form submission, * you can also create two different action methods with a {@link Get @Get} and {@link Post @Post} annotation to * let the framework dispatch the request to the correct method. * <p> * Besides submit detection a form can also recognize a special form of submit which is called reload: * The use case for this are forms which have conditionally parts: Depending on user input * additional controls are presented to the client. To implement this you would typically * submit the form from a Javascript event handler. On the server-side you want to read the request, * add additional controls to the form (depending on the so far entered input), but no error should be triggered yet. */ public class Form implements RequestProvider { static final String RELOADED = "reloaded"; /** * Creates a new form. * @param requestProvider allows the form to access the request and read * parameters from the request. */ public Form(RequestProvider requestProvider) { requestProvider_ = Check.notNull(requestProvider, "requestProvider"); setName("f" + getClass().getName().hashCode()); } //--------------------------------------------- // accessors //--------------------------------------------- /** * Returns the request associated with the form. */ @Override public Request getRequest() { Request request = requestProvider_.getRequest(); if (request == null) throw new IllegalStateException(requestProvider_.getClass() + " returned null request"); return request; } /** * Sets the form name. By default a form name is automatically * generated. When the form is printed, its name is printed * as hidden field. The hidden field then allows to detect * - given a request - if the form was submitted. */ public void setName(String name) { name_ = name; } /** * Returns the form name. By default a unique form name * based on the form class is automatically generated. */ public String getName() { return name_; } /** * Sets the target attribute of the HTML form element. * @see #getTarget() */ public void setTarget(String target) { setAttribute("target", target); } /** * Returns the target attribute of the HTML form element. * The default is null. * @see #setTarget(String) */ public String getTarget() { return Attribute.getValue(attribute_, "target"); } /** * Returns the action attribute of the HTML form element. * The default is null, which in case of a submit will * call the originating resource. */ public String getAction() { return action_; } /** * Sets the action attribute of the form element. It is automatically * URL encoded, when printed. */ public void setAction(String action) { action_ = action; } /** * Returns the method attribute of the form. It is set to POST by default. */ public String getMethod() { return method_; } /** * Sets the value of the method attribute. */ public void setMethod(String method) { method_ = method; } /** * Sets the value of the method attribute to POST. */ public void setPostMethod() { setMethod("POST"); } /** * Sets the value of the method attribute to GET. */ public void setGetMethod() { setMethod("GET"); } /** * Returns the multipart encoded flag previously set by * setMultipartEncoded(). The default value is false. * @see #setMultipartEncoded */ public boolean isMultipartEncoded() { return multipartEncoded_; } /** * Sets if the form is multipart encoded, * i.e. the enctype attribute of the form should have value * "multipart/form-data". If you add a {@link FileField} to * the form, the form is automatically multipart encoded. */ public void setMultipartEncoded(boolean mode) { multipartEncoded_ = mode; } /** * Sets an attribute on the HTML form element. * @param name the attribute name * @param value the attribute value */ public void setAttribute(String name, String value) { attribute_ = new Attribute(name, value, attribute_); } /** * Returns the value of an attribute, previously set * by {@link #setAttribute(String, String)}. */ public String getAttribute(String name) { Attribute attr = Attribute.getAttribute(attribute_, name); return attr == null ? null : attr.value; } /** * Sets the onsubmit attribute to the given javascript expression. * E.g. a call<p> * <code>form.addSubmitCallback("myfunction()");</code><p> * would be printed as<p> * <code><form ... onsubmit="return myfunction();"></code><p> * If you add multiple handlers the expression are chained together: * <code><form ... onsubmit="return myfunction() && myfunction2();"></code>: */ public void addSubmitCallback(String expression) { Attribute attr = Attribute.getAttribute(attribute_, "onsubmit"); if (attr == null) setAttribute("onsubmit", "return " + expression + ';'); else attr.value = attr.value.substring(0, attr.value.length() - 1) + " & " + expression + ';'; } //--------------------------------- // controls //--------------------------------- /** * Returns the number of controls contained in the form. */ public int size() { return controls_.size(); } /** * Returns the control. */ public Control<?> get(int i) { return controls_.get(i); } /** * Returns the control with the given name. */ public Control<?> get(String name) { if (name != null) { int n = size(); for (int i=0; i<n; i++) { Control<?> control = get(i); if (name.equals(control.getName())) return control; } } return null; } /** * Adds a control to the form. * @param control the control. It is ignored if null. */ public <T extends Control<?>> T add(T control) { if (control != null) { control.setForm(this); controls_.add(control); } return control; } /** * Adds a control to the form and sets the label of the control. * @param control the control. It is ignored if null. * @see Control#setLabel(String) */ public <T extends Control<?>> T add(T control, String label) { if (control != null) { control.setLabel(label); return add(control); } else return null; } /** * Removes a control from the form. */ public void remove(Control<?> control) { controls_.remove(control); } /** * Sets the required flag of all controls. */ public void setRequired(boolean required) { int size = size(); for (int i=0; i<size; i++) get(i).setRequired(required); } /** * Sets the read-only flag of all controls. * @see Control#setReadOnly(boolean) */ public void setReadOnly(boolean readOnly) { int size = size(); for (int i=0; i<size; i++) get(i).setReadOnly(readOnly); } //--------------------------------------------- // default button //--------------------------------------------- /** * Returns the default button. The default button is either the first submit button * added to the form or the button which was explicitly set by {@link #setDefaultButton}. * If the form was submitted by pressing enter in a text field then * the {@link Button#isClicked() isClicked()}-method of the default button will return true. */ public Button getDefaultButton() { return defaultButton_; } /** * Sets the default button. * @see #getDefaultButton() */ public void setDefaultButton(Button button) { defaultButton_ = button; } //--------------------------------------------- // request processing //--------------------------------------------- /** * Reads control values from the request. If the form was only reloaded but not submitted, * then the required flag of controls is ignored. * After all control values are read, the {@link #validate(boolean)}-method is called. * @return true if no control has an error when reading its value from the request. * An error could be a type conversion error, a missing value * for a control, or a value which failed the validation * of the control. * @throws Exception thrown when an exception during request processing occurs. */ public boolean read() throws Exception { boolean isReloaded = isReloaded(); boolean ok = true; clearErrorControl(); Request request = getRequest(); int size = size(); for (int i=0; i<size; i++) { Control<?> control = get(i); boolean setRequired = false; if (isReloaded && (setRequired = control.isRequired())) control.setRequired(false); if (!control.read(request)) ok = false; if (setRequired) control.setRequired(true); } if (!isReloaded) ok &= validate(ok); return ok; } /** * Validates the form. Called after all controls have read their values from the request. * The method is not called when a form was reloaded. * The default implementation returns true. * @param ok true if all controls of the form are valid, false if some controls have errors * @return return if true if all controls have valid values, false otherwise. In the later * case you should have changed the {@link Control#setStatus(org.civilian.form.Control.Status) * status} of invalid controls. */ protected boolean validate(boolean ok) throws Exception { return true; } //--------------------------------------------- // status //--------------------------------------------- /** * Returns true if every control is OK. * @see Control#isOk() */ public boolean isOk() { int count = size(); for (int i=0; i<count; i++) { if (!get(i).isOk()) return false; } return true; } /** * Returns true if the form was reloaded. Reload means, that the * form was presented in a HTML page, and based on user interaction * programmatically submitted to the server in order to change, add or remove controls, * and then to be presented again. */ public boolean isReloaded() { return RELOADED.equals(getNameParam()); } /** * Returns true, if the form was submitted. * This test is implemented by checking the request parameter * for the hidden name field of the form. * If it is part of the request then we assume that the form was * submitted. */ public boolean isSubmitted() { return "".equals(getNameParam()); } private String getNameParam() { return name_ != null ? getRequest().getParameter(name_) : null; } //--------------------------------------------- // error control //--------------------------------------------- /** * Returns the first control which contains an error. * The error control is either set by an explicit call to {@link #setErrorControl(Control)} * or when an error status of the control is set ({@link Control#setStatus(org.civilian.form.Control.Status)}. */ public Control<?> getErrorControl() { return errorControl_; } /** * Clears the error control. * @see #getErrorControl() */ public void clearErrorControl() { errorControl_ = null; } /** * Sets the error control of the form. If the form has already * a error control it will be ignored. * @see #getErrorControl() */ public final void setErrorControl(Control<?> control) { if (errorControl_ == null) errorControl_ = control; } //--------------------------------------------- // print //--------------------------------------------- /** * Prints the form start tag and all hidden fields. */ public void start(TemplateWriter out) { start(out, (String[])null); } /** * Prints the form start tag and all hidden fields. * @param attrs a list of attribute names and values which * should be printed in the start tag of the control element. */ public void start(TemplateWriter out, String... attrs) { out.print("<form"); printAttrs(out, attrs); out.println('>'); // each form has a hidden field equal to the form name // it is used to decide if the form was submitted by // hitting enter inside a text field if (name_ != null) { out.print("<input type=\"hidden\" value=\"\""); HtmlUtil.attr(out, "name", name_); out.println(">"); } // print hidden fields int size = size(); for (int i=0; i<size; i++) { Control<?> control = get(i); if (control.getCategory() == Control.Category.HIDDEN) { control.print(out); out.println(); } } } /** * Prints the attributes of the form start tag. */ protected void printAttrs(TemplateWriter out, String... attrs) { Response response = getRequest().getResponse(); if (method_ != null) HtmlUtil.attr(out, "method", method_); String action = getAction(); if (action != null) HtmlUtil.attr(out, "action", response.addSessionId(action)); HtmlUtil.attr(out, "name", name_); if (isMultipartEncoded()) { HtmlUtil.attr(out, "enctype", "multipart/form-data", false); String encoding = response.getContentEncoding(); if (encoding != null) HtmlUtil.attr(out, "accept-charset", encoding); } if (attribute_ != null) out.print(attribute_); HtmlUtil.attrs(out, attrs); } /** * Prints the form end tag. * Calls {@link #end(TemplateWriter, Control) end(out, null)}; */ public void end(TemplateWriter out) { end(out, null); } /** * Prints the form end tag. * If the form contains controls which need scripts to be printed after the end tag, * these scripts are also printed. */ public void end(TemplateWriter out, Control<?> focusControl) { out.println("</form>"); if (getErrorControl() != null) focusControl = getErrorControl(); if (focusControl != null) focusControl = focusControl.toFocusControl(); if (focusControl != null) focusControl.focus(out, true); } private String name_; private String action_; private String method_ = "POST"; private Attribute attribute_; private Button defaultButton_; private Control<?> errorControl_; private boolean multipartEncoded_; private RequestProvider requestProvider_; private ArrayList<Control<?>> controls_ = new ArrayList<>(); }