/* * 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 org.civilian.Request; import org.civilian.template.HtmlUtil; import org.civilian.template.TemplateWriter; import org.civilian.type.ListType; import org.civilian.type.Type; import org.civilian.type.TypeLib; import org.civilian.type.fn.TypeSerializer; import org.civilian.type.fn.StandardSerializer; import org.civilian.util.Check; /** * Control is a base class for classes which model HTML form elements. * A Control class has * <ul> * <li>a name corresponding to the name of the form element. * <li>a {@link #getType() type} * <li>a value of that type * <li>a reference to its {@link Form} * <li>an enabled-flag, * <li>a required-flag, * <li>status information, * <li>the capability to read its value from a Request object, * <li>the capability to print its markup to a TemplateWriter * </ul> */ public abstract class Control<T> implements TemplateWriter.Printable { /** * Status is an Enum for the status of a Control. * @see Control#getStatus() */ public enum Status { /** * OK status. */ OK, /** * A status constant indicating that the Control is required but has no value, * after reading its value from a request. * @see #isRequired() * @see #read(Request) */ MISSING, /** * A status constant indicating that the Control could not * read its value from a Request because of a parse error. * @see #getStatus() * @see #read(Request) */ PARSE_ERROR, /** * A status constant indicating that the Control value * failed validation. * @see #getStatus() * @see Form#validate(boolean) */ VALIDATION_ERROR } /** * Each control has a category, depending on its visual appearance * and function. * @see Control#getCategory() */ public enum Category { /** * A normal control, used to input values. */ INPUT, /** * A button. */ BUTTON, /** * A hidden field. */ HIDDEN, } /** * Creates a new control. * @param type the type of the control */ protected Control(Type<T> type) { type_ = Check.notNull(type, "type"); } /** * Creates a new control. * @param type the type of the controls value * @param name the control name */ protected Control(Type<T> type, String name) { this(type); setName(name); } /** * Returns the type. */ public Type<T> getType() { return type_; } /** * Tests if the given type equals the type of this control. */ protected void checkType(Type<?> type) { if (type_ != type) throw new IllegalArgumentException("invalid type '" + type + "'"); } /** * Returns the name of the Control. */ public String getName() { return name_; } /** * Sets the name of the Control. */ public void setName(String name) { name_ = name; } /** * Sets the form. Called by the form, when the control is {@link Form#add(Control) added} * to the form. */ protected void setForm(Form form) { if (form_ != null) throw new IllegalStateException("already added to form " + form_); form_ = form; } /** * Returns the form to which the control was added. * Returns null when called before the control was added to a form. * @see Form#add(Control) */ public Form getForm() { return form_; } /** * Returns the form to which the control was added. * @throws IllegalStateException when the field was not yet added to a form. */ public Form getSafeForm() { if (form_ == null) throw new IllegalStateException("form is null"); return form_; } /** * Returns the value. */ public T getValue() { return value_; } /** * Returns if the {@link #getValue() control value} is not null. */ public boolean hasValue() { return value_ != null; } /** * Sets the value. */ public void setValue(T value) { setValue(value, null, null); } /** * Unchecked version of setValue() */ @SuppressWarnings("unchecked") private void setValueUc(Object value) { setValue((T)value); } /** * Casts the stored value to a Number and returns its integer value. */ public int getIntValue() { return ((Number)value_).intValue(); } /** * Sets the value. * @throws IllegalArgumentException if the type of this Value * is not Integer. */ public void setIntValue(int value) { checkType(TypeLib.INTEGER); setValueUc(Integer.valueOf(value)); } /** * Casts the stored value to a Boolean and returns its boolean value. */ public boolean getBooleanValue() { return value_ == null ? false : ((Boolean)value_).booleanValue(); } /** * Sets the value of a boolean parameter. * @throws IllegalArgumentException if the type of this Value * is not Boolean. */ public void setBooleanValue(boolean value) { checkType(TypeLib.BOOLEAN); setValueUc(value ? Boolean.TRUE : Boolean.FALSE); } /** * Casts the stored value to a Number and returns its double value. */ public double getDoubleValue() { return ((Number)value_).doubleValue(); } /** * Sets the value of a double parameter. * @throws IllegalArgumentException if the type of this Value * is not Double. */ public void setDoubleValue(double value) { checkType(TypeLib.DOUBLE); setValueUc(new Double(value)); } protected void setValue(T value, Exception error, String errorValue) { value_ = value; error_ = error; errorValue_ = errorValue; updateStatus(); } /** * Returns any parse exception which was caught during a * previous call to parse. */ public Exception getError() { return error_; } /** * Returns the original string value, which was passed to parse() * and caused a parse exception */ public String getErrorValue() { return errorValue_; } /** * Returns if the Value has an error, because of an * unsuccessful parse operation. */ public boolean hasError() { return error_ != null; } /** * Clears any error and error value. */ public void clearError() { setValue(value_, null, null); } /** * Returns the number of rows of the control. The default implementation returns 1. * The number of rows can be used for layout decisions, e.g. for vertical alignment * of a control in a HTML table cell. */ public int getRows() { return 1; } /** * Returns the control category. * The default implementation returns Category.INPUT. */ public Category getCategory() { return Category.INPUT; } /** * Returns the label of the control. * The label is always a non-null string. By default it equals to "". */ public String getLabel() { return label_; } /** * Sets the label of the control. * @return this */ public Control<T> setLabel(String label) { label_ = label == null ? "" : label; return this; } /** * Returns if the control has a label. */ public boolean hasLabel() { return label_.length() > 0; } /** * Returns data associated with the control. * @see #setData(Object) */ public Object getData() { return data_; } /** * Associates the control with arbitrary data. * @return this */ public Control<T> setData(Object data) { data_ = data; return this; } /** * Sets the value of an HTML attribute on the control. * Use this method if the control class does not provide * an explicit access method for that attribute. * @return this */ public Control<T> setAttribute(String name, String value) { attribute_ = new Attribute(name, value, attribute_); return this; } /** * Returns the value of an HTML attribute previously set * by {@link #setAttribute(String, String)}. */ public String getAttribute(String name) { return Attribute.getValue(attribute_, name); } /** * Returns the HTML id of the control. By default it is null. */ public String getId() { return getAttribute("id"); } /** * Sets the HTML id of the control. */ public Control<T> setId(String id) { return setAttribute("id", id); } //----------------------- // request //----------------------- /** * Reads the control value from the request and updates its status. * If the control is marked as disabled or readonly then the method does nothing. * <ul> * <li>If the request value could not be parsed, the status is {@link Status#PARSE_ERROR}. * <li>If the request value is OK the status is {@link Status#OK}. * <li>If there was no request value and the control is not required, the status is {@link Status#OK}. * <li>Else (no request value, but control required) the status is {@link Status#MISSING}. * </ul> * Controls may add custom validation to that logic and could change the status * to {@link Status#VALIDATION_ERROR}, if custom validation fails. Another entry point * for custom validation is {@link Form#validate(boolean)} which is called when the whole * form was {@link Form#read() read} from the request. * @return returns if the new status of the control is Status#OK. * @see #isOk() */ public boolean read(Request request) { if (!isDisabled() && !isReadOnly()) parse(request); return isOk(); } /** * Reads the control value, using the request of the form * to which the field belongs. * @see Control#read(Request) */ public boolean read() { return read(getSafeForm().getRequest()); } /** * Reads the value from the request, but does * not change the value and status in case of an error or a missing value. * @return true iif the Control has a value */ public boolean readDefault(Request request) { T value = getValue(); read(); if (!isOk()) setValue(value); return hasValue(); } /** * Parses the value from the request and updates the status. * Called by read(Request). Overwrite this method if the Control * has a special way to parse a request value. */ protected void parse(Request request) { if (getType().category() != Type.Category.LIST) parseSimple(request); else parseList(request); } private void parseSimple(Request request) { // simple type String s = getRequestString(request); if (s == null) setValue(null); else { try { T value = s != null ? request.getLocaleSerializer().parse(type_, s) : null; setValue(value); } catch(Exception e) { setValue(null, e, s); } } } private <E> void parseList(Request request) { // request.getParameters() always returns a non-null array String s[] = request.getParameters(getName()); try { @SuppressWarnings("unchecked") ListType<T,E> listType = (ListType<T,E>)getType(); E[] values = request.getLocaleSerializer().parseArray(listType.getElementType(), s); setValue(listType.create(values)); } catch(Exception e) { setValue(null, e, null); } } private String getRequestString(Request request) { String s = request.getParameter(getName()); if (s != null) { s = s.trim(); if (s.length() == 0) s = null; } return s; } //------------------------------ // status //------------------------------ /** * Returns if the control is required. A required control with no value * has status STATUS_MISSING. * @see #getStatus() * @see #read(Request) */ public boolean isRequired() { return isRequired_; } /** * Marks the control as required. The default is not required. * The required flag influences the status of the control * when its value is read from a request. */ public Control<T> setRequired(boolean isRequired) { if (isRequired_ != isRequired) { isRequired_ = isRequired; updateStatus(); } return this; } /** * Sets a control required. */ public void setRequired() { setRequired(true); } /** * Returns if the control is read-only. */ public boolean isReadOnly() { return isReadOnly_; } /** * Sets if the control is readonly. */ public Control<T> setReadOnly(boolean isReadOnly) { if (isReadOnly_ != isReadOnly) { isReadOnly_ = isReadOnly; updateStatus(); } return this; } /** * Sets a control to readonly. */ public Control<T> setReadOnly() { return setReadOnly(true); } /** * Returns if the control is disabled. */ public boolean isDisabled() { return isDisabled_; } /** * Sets if the control is disabled. */ public Control<T> setDisabled(boolean disabled) { if (isDisabled_ != disabled) { isDisabled_ = disabled; updateStatus(); } return this; } /** * Sets the control disabled. */ public Control<T> setDisabled() { return setDisabled(true); } //------------------------------ // status //------------------------------ /** * Returns the status of the control. */ public Status getStatus() { return status_; } /** * Returns if the control has a certain status. */ public boolean hasStatus(Status status) { return status_ == status; } /** * Sets the status of the control. * If the field is part of a form, and the status is not Status.OK, * the field will set itself as the forms error control * @see Form#setErrorControl(Control) */ public Control<T> setStatus(Status status) { Check.notNull(status, "status"); status_ = status; if ((status_ != Status.OK) && (form_ != null)) form_.setErrorControl(this); return this; } /** * Called when the value or one of the flags which influence the status * is changed. */ private void updateStatus() { // the control receives status PARSE_ERROR, if // - control has an error set // the control receives status OK, if one of the following is true // - control has a value // - control is disabled // - control is readonly // - control is not required // the control receives status MISSING, if the following is true // - control has no value and is required, enabled, and not readonly if (hasError()) setStatus(Status.PARSE_ERROR); else if (hasValue() || isDisabled_ || isReadOnly_ || !isRequired_) setStatus(Status.OK); else setStatus(Status.MISSING); } /** * Returns if the control has {@link Status#OK}. */ public boolean isOk() { return status_ == Status.OK; } //------------------------------------------ // reload //------------------------------------------ /** * Adds a javascript event handler to the control to reload the form if * the control value changes. * The control must already be added to a form, else an exception is thrown. * Additionally, the control determines if the form was {@link Form#isReloaded() reloaded}. * In this case it reads its value from the request, without setting any error (see {@link #readDefault(Request)}. * If the form was submitted, the field reads its value from the request. */ public void reloadOnChange(T defaultValue) { setAttribute("onchange", "this.form[this.form.name].value = '" + Form.RELOADED + "'; this.form.submit();"); Form form = getSafeForm(); if (form.isReloaded()) readDefault(form.getRequest()); else if (form.isSubmitted()) read(form.getRequest()); } //------------------------------------------ // format //------------------------------------------ /** * Returns the control value formatted as a string. * If the control has an {@link #getErrorValue()} because * it was initialized from an invalid request, the error value * is returned. Else the value converted to a string is returned. */ public String format() { return getErrorValue() != null ? getErrorValue() : formatValue(); } /** * Returns the control value formatted as a string. */ protected String formatValue() { return formatValue(value_); } /** * Returns the control value formatted as a string. */ protected String formatValue(T value) { return getResponseSerializer().format(type_, value, getStyle()); } protected TypeSerializer getResponseSerializer() { return form_ != null ? form_.getRequest().getResponse().getLocaleSerializer() : StandardSerializer.INSTANCE; } /** * Returns a style object which is passed to the TypeSerializer when * formatting a value. */ protected Object getStyle() { return null; } //------------------------------------------ // print //------------------------------------------ /** * Prints the control. */ @Override public void print(TemplateWriter out) { print(out, (String[])null); } /** * Prints the control. * @param attrs a list of attribute names and values which * should be printed in the start tag of the control element. */ public abstract void print(TemplateWriter out, String... attrs); /** * Prints the generic attributes stored in {@link #attribute_} and * runtime attributes. */ protected void printAttrs(TemplateWriter out, String... attrs) { if (attribute_ != null) out.print(attribute_); HtmlUtil.attrs(out, attrs); } /** * Prints a javascript snippet to set the focus to this control. * @param printScript if true the snippet is surrounded by script-tags. */ public void focus(TemplateWriter out, boolean printScript) { if (printScript) out.println("<script>"); out.print("document.forms."); out.print(getSafeForm().getName()); out.print(".elements"); out.print("['"); out.print(getName()); out.println("'].focus();"); if (printScript) out.println("</script>"); } //------------------------------------------ // conversion //------------------------------------------ /** * Returns this control or a sub-control of this control which is able to receive the focus. * Returns null if the control (and none of its sub-controls) may not receive the focus * (e.g. HiddenFields). * The default implementation returns null. */ public Control<?> toFocusControl() { return null; } /** * Returns this control or a subcontrol of this control which is able to receive input from a user. * Returns null if the control (and none of its sub-control) may not receive input. * (e.g. HiddenFields, Buttons). * The default implementation returns this. */ public Control<?> toInputField() { return this; } /** * Returns a debug representation of the control. */ @Override public String toString() { StringBuilder s = new StringBuilder(getClass().getSimpleName()); s.append('['); if (getName() != null) { s.append(getName()); s.append('='); } s.append(getValue()); s.append(']'); return s.toString(); } /** * Tests if two objects are equal */ protected static boolean equals(Object o1, Object o2) { return o1 == null ? o2 == null : o1.equals(o2); } private Type<T> type_; private T value_; private Exception error_; private String errorValue_; private String name_; private boolean isDisabled_; private boolean isReadOnly_; private boolean isRequired_; protected Attribute attribute_; protected String label_ = ""; private Form form_; private Object data_; private Status status_ = Status.OK; }