// Jericho HTML Parser - Java based library for analysing and manipulating HTML // Version 3.2 // Copyright (C) 2004-2009 Martin Jericho // http://jericho.htmlparser.net/ // // This library is free software; you can redistribute it and/or // modify it under the terms of either one of the following licences: // // 1. The Eclipse Public License (EPL) version 1.0, // included in this distribution in the file licence-epl-1.0.html // or available at http://www.eclipse.org/legal/epl-v10.html // // 2. The GNU Lesser General Public License (LGPL) version 2.1 or later, // included in this distribution in the file licence-lgpl-2.1.txt // or available at http://www.gnu.org/licenses/lgpl.txt // // This library is distributed on an "AS IS" basis, // WITHOUT WARRANTY OF ANY KIND, either express or implied. // See the individual licence texts for more details. package net.htmlparser.jericho; import java.util.*; import java.io.*; /** * Represents an HTML form <a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#form-controls">control</a>. * <p> * A <code>FormControl</code> consists of a single {@linkplain #getElement() element} * that matches one of the {@linkplain FormControlType form control types}. * <p> * The term <i><a name="OutputElement">output element</a></i> is used to describe the element that is * {@linkplain OutputSegment#writeTo(Writer) output} if this form control is {@linkplain OutputDocument#replace(FormControl) replaced} * in an {@link OutputDocument}. * <p> * A <i><a name="PredefinedValueControl">predefined value control</a></i> is a form control for which * {@link #getFormControlType()}.{@link FormControlType#hasPredefinedValue() hasPredefinedValue()} * returns <code>true</code>. It has a {@linkplain #getFormControlType() control type} of * {@link FormControlType#CHECKBOX CHECKBOX}, {@link FormControlType#RADIO RADIO}, {@link FormControlType#BUTTON BUTTON}, * {@link FormControlType#SUBMIT SUBMIT}, {@link FormControlType#IMAGE IMAGE}, {@link FormControlType#SELECT_SINGLE SELECT_SINGLE} * or {@link FormControlType#SELECT_MULTIPLE SELECT_MULTIPLE}. * <p> * A <i><a name="UserValueControl">user value control</a></i> is a form control for which * {@link #getFormControlType()}.{@link FormControlType#hasPredefinedValue() hasPredefinedValue()} * returns <code>false</code>. It has a {@linkplain #getFormControlType() control type} of * {@link FormControlType#FILE FILE}, {@link FormControlType#HIDDEN HIDDEN}, {@link FormControlType#PASSWORD PASSWORD}, * {@link FormControlType#TEXT TEXT} or {@link FormControlType#TEXTAREA TEXTAREA}. * <p> * The functionality of most significance to users of this class relates to the * <i><a name="DisplayCharacteristics">display characteristics</a></i> of the <a href="#OutputElement">output element</a>, * manipulated using the {@link #setDisabled(boolean)} and {@link #setOutputStyle(FormControlOutputStyle)} methods. * <p> * As a general rule, the operations dealing with the control's <a href="#SubmissionValue">submission values</a> * are better performed on a {@link FormFields} or {@link FormField} object, which provide a more * intuitive interface by grouping form controls of the same {@linkplain #getName() name} together. * The higher abstraction level of these classes means they can automatically ensure that the * <a href="#SubmissionValue">submission values</a> of their constituent controls are consistent with each other, * for example by ensuring that only one {@link FormControlType#RADIO RADIO} control with a given name is * {@link #isChecked() checked} at a time. * <p> * A {@link FormFields} object can be directly {@linkplain FormFields#FormFields(Collection) constructed} from * a collection of <code>FormControl</code> objects. * <p> * <code>FormControl</code> instances are obtained using the {@link Element#getFormControl()} method or are created automatically * with the creation of a {@link FormFields} object via the {@link Segment#getFormFields()} method. * * @see FormControlType * @see FormFields * @see FormField */ public abstract class FormControl extends Segment { FormControlType formControlType; String name; ElementContainer elementContainer; FormControlOutputStyle outputStyle=FormControlOutputStyle.NORMAL; private static final String CHECKBOX_NULL_DEFAULT_VALUE="on"; private static Comparator<FormControl> COMPARATOR=new PositionComparator(); static FormControl construct(final Element element) { final String tagName=element.getStartTag().getName(); if (tagName==HTMLElementName.INPUT) { final String typeAttributeValue=element.getAttributes().getRawValue(Attribute.TYPE); if (typeAttributeValue==null) return new InputFormControl(element,FormControlType.TEXT); FormControlType formControlType=FormControlType.getFromInputElementType(typeAttributeValue); if (formControlType==null) { if (formControlType.isNonFormControl(typeAttributeValue)) return null; if (element.source.logger.isInfoEnabled()) element.source.logger.info(element.source.getRowColumnVector(element.begin).appendTo(new StringBuilder(200)).append(": INPUT control with unrecognised type \"").append(typeAttributeValue).append("\" assumed to be type \"text\"").toString()); formControlType=FormControlType.TEXT; } switch (formControlType) { case TEXT: return new InputFormControl(element,formControlType); case CHECKBOX: case RADIO: return new RadioCheckboxFormControl(element,formControlType); case SUBMIT: return new SubmitFormControl(element,formControlType); case IMAGE: return new ImageSubmitFormControl(element); case HIDDEN: case PASSWORD: case FILE: return new InputFormControl(element,formControlType); default: throw new AssertionError(formControlType); } } else if (tagName==HTMLElementName.SELECT) { return new SelectFormControl(element); } else if (tagName==HTMLElementName.TEXTAREA) { return new TextAreaFormControl(element); } else if (tagName==HTMLElementName.BUTTON) { return "submit".equalsIgnoreCase(element.getAttributes().getRawValue(Attribute.TYPE)) ? new SubmitFormControl(element,FormControlType.BUTTON) : null; } else { return null; } } private FormControl(final Element element, final FormControlType formControlType, final boolean loadPredefinedValue) { super(element.source,element.begin,element.end); elementContainer=new ElementContainer(element,loadPredefinedValue); this.formControlType=formControlType; name=element.getAttributes().getValue(Attribute.NAME); verifyName(); } /** * Returns the {@linkplain FormControlType type} of this form control. * @return the {@linkplain FormControlType type} of this form control. */ public final FormControlType getFormControlType() { return formControlType; } /** * Returns the <a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#control-name">name</a> of the control. * <p> * The name comes from the value of the <code>name</code> {@linkplain Attribute attribute} of the * {@linkplain #getElement() form control's element}, not the {@linkplain Element#getName() name of the element} itself. * <p> * Since a {@link FormField} is simply a group of controls with the same name, the terms <i>control name</i> and * <i>field name</i> are for the most part synonymous, with only a possible difference in case differentiating them. * <p> * In contrast to the {@link FormField#getName()} method, this method always returns the name using the original case * from the source document, regardless of the current setting of the static * {@link Config#CurrentCompatibilityMode}<code>.</code>{@link Config.CompatibilityMode#isFormFieldNameCaseInsensitive() FormFieldNameCaseInsensitive} property. * * @return the <a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#control-name">name</a> of the control. */ public final String getName() { return name; } /** * Returns the {@linkplain Element element} representing this form control in the source document. * <p> * The {@linkplain Element#getAttributes() attributes} of this source element should correspond with the * <a href="#OutputAttributes">output attributes</a> if the * <a href="#DisplayCharacteristics">display characteristics</a> or <a href="FormField.html#SubmissionValue">submission values</a> * have not been modified. * * @return the {@linkplain Element element} representing this form control in the source document. */ public final Element getElement() { return elementContainer.element; } /** * Returns an iterator over the {@link HTMLElementName#OPTION OPTION} {@linkplain Element elements} contained within this control, in order of appearance. * <p> * This method is only relevant to form controls with a {@linkplain #getFormControlType() type} of * {@link FormControlType#SELECT_SINGLE SELECT_SINGLE} or {@link FormControlType#SELECT_MULTIPLE SELECT_MULTIPLE}. * * @return an iterator over the {@link HTMLElementName#OPTION OPTION} {@linkplain Element elements} contained within this control, in order of appearance. * @throws UnsupportedOperationException if the {@linkplain #getFormControlType() type} of this control is not {@link FormControlType#SELECT_SINGLE SELECT_SINGLE} or {@link FormControlType#SELECT_MULTIPLE SELECT_MULTIPLE}. */ public Iterator<Element> getOptionElementIterator() { // overridden in SelectFormControl throw new UnsupportedOperationException("Only SELECT controls contain OPTION elements"); } /** * Returns the current {@linkplain FormControlOutputStyle output style} of this form control. * <p> * This property affects how this form control is displayed if it has been {@linkplain OutputDocument#replace(FormControl) replaced} * in an {@link OutputDocument}. * See the documentation of the {@link FormControlOutputStyle} class for information on the available output styles. * <p> * The default output style for every form control is {@link FormControlOutputStyle#NORMAL}. * * @return the current {@linkplain FormControlOutputStyle output style} of this form control. * @see #setOutputStyle(FormControlOutputStyle) */ public FormControlOutputStyle getOutputStyle() { return outputStyle; } /** * Sets the {@linkplain FormControlOutputStyle output style} of this form control. * <p> * See the {@link #getOutputStyle()} method for a full description of this property. * * @param outputStyle the new {@linkplain FormControlOutputStyle output style} of this form control. */ public void setOutputStyle(final FormControlOutputStyle outputStyle) { this.outputStyle=outputStyle; } /** * Returns a map of the names and values of this form control's <a href="#OutputAttributes">output attributes</a>. * <p> * The term <i><a name="OutputAttributes">output attributes</a></i> is used in this library to refer to the * <a target="_blank" href="http://www.w3.org/TR/html401/intro/sgmltut.html#h-3.2.2">attributes</a> of a form control's * <a href="#OutputElement">output element</a>. * <p> * The map keys are the <code>String</code> attribute names, which should all be in lower case. * The map values are the corresponding <code>String</code> attribute values, with a <code>null</code> value given * to an attribute that {@linkplain Attribute#hasValue() has no value}. * <p> * Direct manipulation of the returned map affects the attributes of this form control's <a href="#OutputElement">output element</a>. * It is the responsibility of the user to ensure that all entries added to the map use the correct key and value types, * and that all keys (attribute names) are in lower case. * <p> * It is recommended that the <a href="#SubmissionValueModificationMethods">submission value modification methods</a> * are used to modify attributes that affect the <a href="#SubmissionValue">submission value</a> of the control * rather than manipulating the attributes map directly. * <p> * An iteration over the map entries will return the attributes in the same order as they appeared in the source document, or * at the end if the attribute was not present in the source document. * <p> * The returned attributes only correspond with those of the {@linkplain #getElement() source element} if the control's * <a href="#DisplayCharacteristics">display characteristics</a> and <a href="#SubmissionValue">submission values</a> * have not been modified. * * @return a map of the names and values of this form control's <a href="#OutputAttributes">output attributes</a>. */ public final Map<String,String> getAttributesMap() { return elementContainer.getAttributesMap(); } /** * Indicates whether this form control is <a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#adef-disabled">disabled</a>. * <p> * The form control is disabled if the attribute * "<code><a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#adef-disabled">disabled</a></code>" * is present in its <a href="#OutputElement">output element</a>. * <p> * The return value is is logically equivalent to {@link #getAttributesMap()}<code>.containsKey("disabled")</code>, * but using this property may be more efficient in some circumstances. * * @return <code>true</code> if this form control is <a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#adef-disabled">disabled</a>, otherwise <code>false</code>. */ public final boolean isDisabled() { return elementContainer.getBooleanAttribute(Attribute.DISABLED); } /** * Sets whether this form control is <a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#adef-disabled">disabled</a>. * <p> * If the argument supplied to this method is <code>true</code> and the <code>disabled</code> attribute is not already present * in the output element, the full * <a target="_blank" href="http://www.w3.org/TR/xhtml1/">XHTML</a> compatible attribute <code>disabled="disabled"</code> is added. * If the attribute is already present, it is left unchanged. * <p> * If the argument supplied to this method is <code>false</code>, the attribute is removed from the output element. * <p> * See the {@link #isDisabled()} method for more information. * * @param disabled the new value of this property. */ public final void setDisabled(final boolean disabled) { elementContainer.setBooleanAttribute(Attribute.DISABLED,disabled); } /** * Indicates whether this form control is <a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#adef-checked">checked</a>. * <p> * The term <i>checked</i> is used to describe a checkbox or radio button control that is selected, which is the case if the attribute * "<code><a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#adef-checked">checked</a></code>" * is present in its <a href="#OutputElement">output element</a>. * <p> * This property is only relevant to form controls with a {@linkplain #getFormControlType() type} of * {@link FormControlType#CHECKBOX} or {@link FormControlType#RADIO}, and throws an <code>UnsupportedOperationException</code> * for other control types. * <p> * Use one of the <a href="#SubmissionValueModificationMethods">submission value modification methods</a> to change the value * of this property. * <p> * If this control is a checkbox, you can set it to checked by calling * {@link #setValue(String) setValue}<code>(</code>{@link #getName()}<code>)</code>, and set it to unchecked by calling * {@link #clearValues()}. * <p> * If this control is a radio button, you should use the {@link FormField#setValue(String)} method or one of the other * higher level <a href="#SubmissionValueModificationMethods">submission value modification methods</a> * to set the control to checked, as calling {@link #setValue(String)} method on this object * in the same way as for a checkbox does not automatically uncheck all other radio buttons with the same name. * Even calling {@link #clearValues()} on this object to ensure that this radio button is unchecked is not recommended, as * it can lead to a situation where all the radio buttons with this name are unchecked. * The <a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#radio">HTML 4.01 specification of radio buttons</a> * recommends against this situation because it is not defined how user agents should handle it, and behaviour differs amongst browsers. * <p> * The return value is logically equivalent to {@link #getAttributesMap()}<code>.containsKey("checked")</code>, * but using this property may be more efficient in some circumstances. * * @return <code>true</code> if this form control is <a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#adef-checked">checked</a>, otherwise <code>false</code>. * @throws UnsupportedOperationException if the {@linkplain #getFormControlType() type} of this control is not {@link FormControlType#CHECKBOX} or {@link FormControlType#RADIO}. */ public boolean isChecked() { throw new UnsupportedOperationException("This property is only relevant for CHECKBOX and RADIO controls"); } /** * Returns the <a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#initial-value">initial value</a> of this control if it has a {@linkplain FormControlType#hasPredefinedValue() predefined value}. * <p> * Only <a href="#PredefinedValueControl">predefined value controls</a> can return a non-<code>null</code> result. * All other control types return <code>null</code>. * <p> * {@link FormControlType#CHECKBOX CHECKBOX} and {@link FormControlType#RADIO RADIO} controls have a guaranteed * predefined value determined by the value of its compulsory * <code><a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#adef-value-INPUT">value</a></code> * attribute. If the attribute is not present in the source document, this library assigns the control a default * predefined value of "<code>on</code>", consistent with popular browsers. * <p> * {@link FormControlType#SUBMIT SUBMIT}, {@link FormControlType#BUTTON BUTTON} and {@link FormControlType#IMAGE IMAGE} * controls have an optional predefined value determined by the value of its * <code><a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#adef-value-INPUT">value</a></code> * attribute. This value is * <a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#successful-controls">successful</a> * only in the control used to submit the form. * <p> * {@link FormControlType#SELECT_SINGLE} and {@link FormControlType#SELECT_MULTIPLE} controls are special cases * because they usually contain multiple * <code><a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#edef-OPTION">OPTION</a></code> * elements, each with its own predefined value. * In this case the {@link #getPredefinedValues()} method should be used instead, which returns a collection of all the * control's predefined values. Attempting to call this method on a <code>SELECT</code> control results in * a <code>java.lang.UnsupportedOperationException</code>. * <p> * The predefined value of a control is not affected by changes to the * <a href="#SubmissionValue">submission value</a> of the control. * * @return the <a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#initial-value">initial value</a> of this control if it has a {@linkplain FormControlType#hasPredefinedValue() predefined value}, or <code>null</code> if none. */ public String getPredefinedValue() { return elementContainer.predefinedValue; } /** * Returns a collection of all {@linkplain #getPredefinedValue() predefined values} in this control in order of appearance. * <p> * All objects in the returned collection are of type <code>String</code>, with no <code>null</code> entries. * <p> * This method is most useful for * <code><a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#edef-SELECT">SELECT</a></code> * controls since they typically contain multiple predefined values. * In other controls it returns a collection with zero or one item based on the output of the * {@link #getPredefinedValue()} method, so for efficiency it is recommended to use the * {@link #getPredefinedValue()} method instead. * <p> * The multiple predefined values of a * <code><a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#edef-SELECT">SELECT</a></code> * control are defined by the * <code><a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#edef-OPTION">OPTION</a></code> * elements within it. * Each <code>OPTION</code> element has an * <a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#initial-value">initial value</a> * determined by the value of its * <code><a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#adef-value-OPTION">value</a></code> * attribute, or if this attribute is not present, by its * {@linkplain CharacterReference#decode(CharSequence) decoded} {@linkplain Element#getContent() content} * text with {@linkplain CharacterReference#decodeCollapseWhiteSpace(CharSequence) collapsed white space}. * <p> * The predefined values of a control are not affected by changes to the * <a href="#SubmissionValue">submission values</a> of the control. * * @return a collection of all {@linkplain #getPredefinedValue() predefined values} in this control in order of appearance, guaranteed not <code>null</code>. * @see FormField#getPredefinedValues() */ public Collection<String> getPredefinedValues() { if (getPredefinedValue()==null) Collections.emptySet(); return Collections.singleton(getPredefinedValue()); } /** * Returns a list of the control's <a href="#SubmissionValue">submission values</a> in order of appearance. * <p> * All objects in the returned list are of type <code>String</code>, with no <code>null</code> entries. * <p> * The term <i><a name="SubmissionValue">submission value</a></i> is used in this library to refer to the value the control * would contribute to the * <a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#form-data-set">form data set</a> * of a <a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#submit-format">submitted</a> * form, assuming no modification of the control's * <a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#current-value">current value</a> by the * <a target="_blank" href="http://www.w3.org/TR/html401/conform.html#didx-user_agent">user agent</a> or by end user interaction. * <p> * For <a href="#UserValueControl">user value controls</a>, the submission value corresponds to the * control's <a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#initial-value">initial value</a>. * <p> * The definition of the submission value for each <a href="#PredefinedValueControl">predefined value control</a> type is as follows: * <p> * {@link FormControlType#CHECKBOX CHECKBOX} and {@link FormControlType#RADIO RADIO} controls * have a submission value specified by its {@linkplain #getPredefinedValue() predefined value} * if it is {@link #isChecked() checked}, otherwise it has no submission value. * <p> * {@link FormControlType#SELECT_SINGLE SELECT_SINGLE} and {@link FormControlType#SELECT_MULTIPLE SELECT_MULTIPLE} controls * have submission values specified by the * <a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#adef-value-OPTION">values</a> of the control's * <a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#adef-selected">selected</a> * <code>OPTION</code> elements. * <p> * Only a {@link FormControlType#SELECT_MULTIPLE SELECT_MULTIPLE} control can have more than one submission value, * all other {@linkplain FormControlType control types} return a list containing either one value or no values. * A {@link FormControlType#SELECT_SINGLE SELECT_SINGLE} control only returns multiple submission values * if it illegally contains multiple selected options in the source document. * <p> * {@link FormControlType#SUBMIT SUBMIT}, {@link FormControlType#BUTTON BUTTON}, and {@link FormControlType#IMAGE IMAGE} * controls are only ever * <a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#successful-controls">successful</a> * when they are activated by the user to * <a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#submit-format">submit</a> the form. * Because the submission value is intended to be a static representation of a control's data without * interaction by the user, this library never associates submission values with * {@linkplain FormControlType#isSubmit() submit} buttons, so this method always returns an empty list for these * control types. * <p> * The <a href="#SubmissionValue">submission value(s)</a> of a control can be modified for subsequent output in * an {@link OutputDocument} using the various * <i><a name="SubmissionValueModificationMethods">submission value modification methods</a></i>, namely:<br /> * {@link FormField#setValue(String)}<br /> * {@link FormField#addValue(String)}<br /> * {@link FormField#setValues(Collection)}<br /> * {@link FormField#clearValues()}<br /> * {@link FormFields#setValue(String fieldName, String value)}<br /> * {@link FormFields#addValue(String fieldName, String value)}<br /> * {@link FormFields#setDataSet(Map)}<br /> * {@link FormFields#clearValues()}<br /> * {@link #setValue(String) FormControl.setValue(String)}<br /> * {@link #addValue(String) FormControl.addValue(String)}<br /> * {@link #clearValues() FormControl.clearValues()}<br /> * <p> * The values returned by this method reflect any changes made using the submission value modification methods, * in contrast to methods found in the {@link Attributes} and {@link Attribute} classes, which always reflect the source document. * * @return a list of the control's <i>submission values</i> in order of appearance, guaranteed not <code>null</code>. * @see #getPredefinedValues() */ public List<String> getValues() { final List<String> values=new ArrayList<String>(); addValuesTo(values); return values; } /** * Clears the control's existing <a href="#SubmissionValue">submission values</a>. * <p> * This is equivalent to {@link #setValue(String) setValue(null)}. * <p> * NOTE: The {@link FormFields} and {@link FormField} classes provide a more appropriate abstraction level for the modification of form control submission values. * * @see FormFields#clearValues() * @see FormField#clearValues() */ public final void clearValues() { setValue(null); } /** * Sets the control's <a href="#SubmissionValue">submission value</a> *. * <p> * * NOTE: The {@link FormFields} and {@link FormField} classes provide a more appropriate abstraction level for the modification of form control submission values. * Consider using the {@link FormFields#setValue(String fieldName, String value)} method instead. * <p> * The specified value replaces any existing <a href="#SubmissionValue">submission values</a> of the control. * <p> * The return value indicates whether the control has "accepted" the value. * For <a href="#UserValueControl">user value controls</a>, the return value is always <code>true</code>. * <p> * For <a href="#PredefinedValueControl">predefined value controls</a>, * calling this method does not affect the control's * {@linkplain #getPredefinedValues() predefined values}, but instead determines whether the control (or its options) become * <code><a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#adef-checked">checked</a></code> or * <code><a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#adef-selected">selected</a></code> * as detailed below: * <p> * {@link FormControlType#CHECKBOX CHECKBOX} and {@link FormControlType#RADIO RADIO} controls become {@link #isChecked() checked} * and the method returns <code>true</code> if the specified value matches the control's predefined value (case sensitive), * otherwise the control becomes unchecked and the method returns <code>false</code>. * Note that any other controls with the same {@linkplain #getName() name} are not unchecked if this control becomes checked, * possibly resulting in an invalid state where multiple <code>RADIO</code> controls are checked at the same time. * The {@link FormField#setValue(String)} method avoids such problems and its use is recommended over this method. * <p> * {@link FormControlType#SELECT_SINGLE SELECT_SINGLE} and {@link FormControlType#SELECT_MULTIPLE SELECT_MULTIPLE} * controls receive the specified value by selecting the option with the matching value and deselecting all others. * If none of the options match, all are deselected. * The return value of this method indicates whether one of the options matched. * <p> * {@link FormControlType#SUBMIT SUBMIT}, {@link FormControlType#BUTTON BUTTON}, and {@link FormControlType#IMAGE IMAGE} * controls never have a <a href="#SubmissionValue">submission value</a>, so calling this method has no effect and * always returns <code>false</code>. * * @param value the new <a href="#SubmissionValue">submission value</a> of this control, or <code>null</code> to clear the control of all submission values. * @return <code>true</code> if the control accepts the value, otherwise <code>false</code>. * @see FormFields#setValue(String fieldName, String value) */ public abstract boolean setValue(String value); /** * Adds the specified value to this control's <a href="#SubmissionValue">submission values</a> *. * <p> * * NOTE: The {@link FormFields} and {@link FormField} classes provide a more appropriate abstraction level for the modification of form control submission values. * Consider using the {@link FormFields#addValue(String fieldName, String value)} method instead. * <p> * This is almost equivalent to {@link #setValue(String)}, with only the following differences: * <p> * {@link FormControlType#CHECKBOX CHECKBOX} controls retain their existing <a href="#SubmissionValue">submission value</a> * instead of becoming unchecked if the specified value does not match the control's {@linkplain #getPredefinedValue() predefined value}. * <p> * {@link FormControlType#SELECT_MULTIPLE SELECT_MULTIPLE} controls retain their existing * <a href="#SubmissionValue">submission values</a>, meaning that the control's * <code><a target="_blank" href="http://www.w3.org/TR/html401/interact/forms.html#edef-OPTION">OPTION</a></code> * elements whose {@linkplain #getPredefinedValues() predefined values} do not match the specified value are not deselected. * This is the only type of control that can have multiple submission values within the one control. * * @param value the value to add to this control's <a href="#SubmissionValue">submission values</a>, must not be <code>null</code>. * @return <code>true</code> if the control accepts the value, otherwise <code>false</code>. * @see FormFields#addValue(String fieldName, String value) */ public boolean addValue(final String value) { return setValue(value); } abstract void addValuesTo(Collection<String> collection); // should not add null values abstract void addToFormFields(FormFields formFields); abstract void replaceInOutputDocument(OutputDocument outputDocument); public String getDebugInfo() { final StringBuilder sb=new StringBuilder(); sb.append(formControlType).append(" name=\"").append(name).append('"'); if (elementContainer.predefinedValue!=null) sb.append(" PredefinedValue=\"").append(elementContainer.predefinedValue).append('"'); sb.append(" - ").append(getElement().getDebugInfo()); return sb.toString(); } static final class InputFormControl extends FormControl { // TEXT, HIDDEN, PASSORD or FILE public InputFormControl(final Element element, final FormControlType formControlType) { super(element,formControlType,false); } public boolean setValue(final String value) { elementContainer.setAttributeValue(Attribute.VALUE,value); return true; } void addValuesTo(final Collection<String> collection) { addValueTo(collection,elementContainer.getAttributeValue(Attribute.VALUE)); } void addToFormFields(final FormFields formFields) { formFields.add(this); } void replaceInOutputDocument(final OutputDocument outputDocument) { if (outputStyle==FormControlOutputStyle.REMOVE) { outputDocument.remove(getElement()); } else if (outputStyle==FormControlOutputStyle.DISPLAY_VALUE) { String output=null; if (formControlType!=FormControlType.HIDDEN) { String value=elementContainer.getAttributeValue(Attribute.VALUE); if (formControlType==FormControlType.PASSWORD && value!=null) value=getString(FormControlOutputStyle.ConfigDisplayValue.PasswordChar,value.length()); output=getDisplayValueHTML(value,false); } outputDocument.replace(getElement(),output); } else { replaceAttributesInOutputDocumentIfModified(outputDocument); } } } static final class TextAreaFormControl extends FormControl { // TEXTAREA public String value=UNCHANGED; private static final String UNCHANGED=new String(); public TextAreaFormControl(final Element element) { super(element,FormControlType.TEXTAREA,false); } public boolean setValue(final String value) { this.value=value; return true; } void addValuesTo(final Collection<String> collection) { addValueTo(collection,getValue()); } void addToFormFields(final FormFields formFields) { formFields.add(this); } void replaceInOutputDocument(final OutputDocument outputDocument) { if (outputStyle==FormControlOutputStyle.REMOVE) { outputDocument.remove(getElement()); } else if (outputStyle==FormControlOutputStyle.DISPLAY_VALUE) { outputDocument.replace(getElement(),getDisplayValueHTML(getValue(),true)); } else { replaceAttributesInOutputDocumentIfModified(outputDocument); if (value!=UNCHANGED) outputDocument.replace(getElement().getContent(),CharacterReference.encode(value)); } } private String getValue() { return (value==UNCHANGED) ? CharacterReference.decode(getElement().getContent()) : value; } } static final class RadioCheckboxFormControl extends FormControl { // RADIO or CHECKBOX public RadioCheckboxFormControl(final Element element, final FormControlType formControlType) { super(element,formControlType,true); if (elementContainer.predefinedValue==null) { elementContainer.predefinedValue=CHECKBOX_NULL_DEFAULT_VALUE; if (element.source.logger.isInfoEnabled()) element.source.logger.info(element.source.getRowColumnVector(element.begin).appendTo(new StringBuilder(200)).append(": compulsory \"value\" attribute of ").append(formControlType).append(" control \"").append(name).append("\" is missing, assuming the value \"").append(CHECKBOX_NULL_DEFAULT_VALUE).append('"').toString()); } } public boolean setValue(final String value) { return elementContainer.setSelected(value,Attribute.CHECKED,false); } public boolean addValue(final String value) { return elementContainer.setSelected(value,Attribute.CHECKED,formControlType==FormControlType.CHECKBOX); } void addValuesTo(final Collection<String> collection) { if (isChecked()) addValueTo(collection,getPredefinedValue()); } public boolean isChecked() { return elementContainer.getBooleanAttribute(Attribute.CHECKED); } void addToFormFields(final FormFields formFields) { formFields.add(this); } void replaceInOutputDocument(final OutputDocument outputDocument) { if (outputStyle==FormControlOutputStyle.REMOVE) { outputDocument.remove(getElement()); } else { if (outputStyle==FormControlOutputStyle.DISPLAY_VALUE) { final String html=isChecked() ? FormControlOutputStyle.ConfigDisplayValue.CheckedHTML : FormControlOutputStyle.ConfigDisplayValue.UncheckedHTML; if (html!=null) { outputDocument.replace(getElement(),html); return; } setDisabled(true); } replaceAttributesInOutputDocumentIfModified(outputDocument); } } } static class SubmitFormControl extends FormControl { // BUTTON, SUBMIT or (in subclass) IMAGE public SubmitFormControl(final Element element, final FormControlType formControlType) { super(element,formControlType,true); } public boolean setValue(final String value) { return false; } void addValuesTo(final Collection<String> collection) {} void addToFormFields(final FormFields formFields) { if (getPredefinedValue()!=null) formFields.add(this); } void replaceInOutputDocument(final OutputDocument outputDocument) { if (outputStyle==FormControlOutputStyle.REMOVE) { outputDocument.remove(getElement()); } else { if (outputStyle==FormControlOutputStyle.DISPLAY_VALUE) setDisabled(true); replaceAttributesInOutputDocumentIfModified(outputDocument); } } } static final class ImageSubmitFormControl extends SubmitFormControl { // IMAGE public ImageSubmitFormControl(final Element element) { super(element,FormControlType.IMAGE); } void addToFormFields(final FormFields formFields) { super.addToFormFields(formFields); formFields.addName(this,name+".x"); formFields.addName(this,name+".y"); } } static final class SelectFormControl extends FormControl { // SELECT_MULTIPLE or SELECT_SINGLE public ElementContainer[] optionElementContainers; public SelectFormControl(final Element element) { super(element,element.getAttributes().get(Attribute.MULTIPLE)!=null ? FormControlType.SELECT_MULTIPLE : FormControlType.SELECT_SINGLE,false); final List<Element> optionElements=element.getAllElements(HTMLElementName.OPTION); optionElementContainers=new ElementContainer[optionElements.size()]; int x=0; for (Element optionElement : optionElements) { final ElementContainer optionElementContainer=new ElementContainer(optionElement,true); if (optionElementContainer.predefinedValue==null) // use the content of the element if it has no value attribute optionElementContainer.predefinedValue=CharacterReference.decodeCollapseWhiteSpace(optionElementContainer.element.getContent()); optionElementContainers[x++]=optionElementContainer; } } public String getPredefinedValue() { throw new UnsupportedOperationException("Use getPredefinedValues() method instead on SELECT controls"); } public Collection<String> getPredefinedValues() { final LinkedHashSet<String> linkedHashSet=new LinkedHashSet<String>(optionElementContainers.length*2,1.0F); for (int i=0; i<optionElementContainers.length; i++) linkedHashSet.add(optionElementContainers[i].predefinedValue); return linkedHashSet; } public Iterator<Element> getOptionElementIterator() { return new OptionElementIterator(); } public boolean setValue(final String value) { return addValue(value,false); } public boolean addValue(final String value) { return addValue(value,formControlType==FormControlType.SELECT_MULTIPLE); } private boolean addValue(final String value, final boolean allowMultipleValues) { boolean valueFound=false; for (int i=0; i<optionElementContainers.length; i++) { if (optionElementContainers[i].setSelected(value,Attribute.SELECTED,allowMultipleValues)) valueFound=true; } return valueFound; } void addValuesTo(final Collection<String> collection) { for (int i=0; i<optionElementContainers.length; i++) { if (optionElementContainers[i].getBooleanAttribute(Attribute.SELECTED)) addValueTo(collection,optionElementContainers[i].predefinedValue); } } void addToFormFields(final FormFields formFields) { for (int i=0; i<optionElementContainers.length; i++) formFields.add(this,optionElementContainers[i].predefinedValue); } void replaceInOutputDocument(final OutputDocument outputDocument) { if (outputStyle==FormControlOutputStyle.REMOVE) { outputDocument.remove(getElement()); } else if (outputStyle==FormControlOutputStyle.DISPLAY_VALUE) { final StringBuilder sb=new StringBuilder(100); for (int i=0; i<optionElementContainers.length; i++) { if (optionElementContainers[i].getBooleanAttribute(Attribute.SELECTED)) { sb.append(getOptionLabel(optionElementContainers[i].element)); sb.append(FormControlOutputStyle.ConfigDisplayValue.MultipleValueSeparator); } } if (sb.length()>0) sb.setLength(sb.length()-FormControlOutputStyle.ConfigDisplayValue.MultipleValueSeparator.length()); // remove last separator outputDocument.replace(getElement(),getDisplayValueHTML(sb,false)); } else { replaceAttributesInOutputDocumentIfModified(outputDocument); for (int i=0; i<optionElementContainers.length; i++) { optionElementContainers[i].replaceAttributesInOutputDocumentIfModified(outputDocument); } } } private static String getOptionLabel(final Element optionElement) { final String labelAttributeValue=optionElement.getAttributeValue("label"); if (labelAttributeValue!=null) return labelAttributeValue; return CharacterReference.decodeCollapseWhiteSpace(optionElement.getContent()); } private final class OptionElementIterator implements Iterator<Element> { private int i=0; public boolean hasNext() { return i<optionElementContainers.length; } public Element next() { if (!hasNext()) throw new NoSuchElementException(); return optionElementContainers[i++].element; } public void remove() { throw new UnsupportedOperationException(); } } } final String getDisplayValueHTML(final CharSequence text, final boolean whiteSpaceFormatting) { final StringBuilder sb=new StringBuilder((text==null ? 0 : text.length()*2)+50); sb.append('<').append(FormControlOutputStyle.ConfigDisplayValue.ElementName); try { for (String attributeName : FormControlOutputStyle.ConfigDisplayValue.AttributeNames) { final CharSequence attributeValue=elementContainer.getAttributeValue(attributeName); if (attributeValue==null) continue; Attribute.appendHTML(sb,attributeName,attributeValue); } sb.append('>'); if (text==null || text.length()==0) sb.append(FormControlOutputStyle.ConfigDisplayValue.EmptyHTML); else CharacterReference.appendEncode(sb,text,whiteSpaceFormatting); } catch (IOException ex) {throw new RuntimeException(ex);} // never happens sb.append(EndTagType.START_DELIMITER_PREFIX).append(FormControlOutputStyle.ConfigDisplayValue.ElementName).append('>'); return sb.toString(); } final void replaceAttributesInOutputDocumentIfModified(final OutputDocument outputDocument) { elementContainer.replaceAttributesInOutputDocumentIfModified(outputDocument); } static List<FormControl> getAll(final Segment segment) { final ArrayList<FormControl> list=new ArrayList<FormControl>(); getAll(segment,list,HTMLElementName.INPUT); getAll(segment,list,HTMLElementName.TEXTAREA); getAll(segment,list,HTMLElementName.SELECT); getAll(segment,list,HTMLElementName.BUTTON); Collections.sort(list,COMPARATOR); return list; } private static void getAll(final Segment segment, final ArrayList<FormControl> list, final String tagName) { for (Element element : segment.getAllElements(tagName)) { final FormControl formControl=element.getFormControl(); if (formControl!=null) list.add(formControl); } } private static String getString(final char ch, final int length) { if (length==0) return ""; final StringBuilder sb=new StringBuilder(length); for (int i=0; i<length; i++) sb.append(ch); return sb.toString(); } private void verifyName() { if (formControlType.isSubmit()) return; String missingOrBlank; if (name==null) { missingOrBlank="missing"; } else { if (name.length()!=0) return; missingOrBlank="blank"; } final Source source=getElement().source; if (source.logger.isInfoEnabled()) source.logger.info(getElement().source.getRowColumnVector(getElement().begin).appendTo(new StringBuilder(200)).append(": compulsory \"name\" attribute of ").append(formControlType).append(" control is ").append(missingOrBlank).toString()); } private static final void addValueTo(final Collection<String> collection, final String value) { collection.add(value!=null ? value : ""); } private static final class PositionComparator implements Comparator<FormControl> { public int compare(final FormControl formControl1, final FormControl formControl2) { final int formControl1Begin=formControl1.getElement().getBegin(); final int formControl2Begin=formControl2.getElement().getBegin(); if (formControl1Begin<formControl2Begin) return -1; if (formControl1Begin>formControl2Begin) return 1; return 0; } } ////////////////////////////////////////////////////////////////////////////////////// static final class ElementContainer { // Contains the information common to both a FormControl and to each OPTION element // within a SELECT FormControl public final Element element; public Map<String,String> attributesMap=null; public String predefinedValue; // never null for option, checkbox or radio elements public ElementContainer(final Element element, final boolean loadPredefinedValue) { this.element=element; predefinedValue=loadPredefinedValue ? element.getAttributes().getValue(Attribute.VALUE) : null; } public Map<String,String> getAttributesMap() { if (attributesMap==null) attributesMap=element.getAttributes().getMap(true); return attributesMap; } public boolean setSelected(final String value, final String selectedOrChecked, final boolean allowMultipleValues) { if (value!=null && predefinedValue.equals(value.toString())) { setBooleanAttribute(selectedOrChecked,true); return true; } if (!allowMultipleValues) setBooleanAttribute(selectedOrChecked,false); return false; } public String getAttributeValue(final String attributeName) { if (attributesMap!=null) return attributesMap.get(attributeName); else return element.getAttributes().getValue(attributeName); } public void setAttributeValue(final String attributeName, final String value) { // null value indicates attribute should be removed. if (value==null) { setBooleanAttribute(attributeName,false); return; } if (attributesMap!=null) { attributesMap.put(attributeName,value); return; } final String existingValue=getAttributeValue(attributeName); if (existingValue!=null && existingValue.equals(value)) return; getAttributesMap().put(attributeName,value); } public boolean getBooleanAttribute(final String attributeName) { if (attributesMap!=null) return attributesMap.containsKey(attributeName); else return element.getAttributes().get(attributeName)!=null; } public void setBooleanAttribute(final String attributeName, final boolean value) { final boolean oldValue=getBooleanAttribute(attributeName); if (value==oldValue) return; if (value) getAttributesMap().put(attributeName,attributeName); // xhtml compatible attribute else getAttributesMap().remove(attributeName); } public void replaceAttributesInOutputDocumentIfModified(final OutputDocument outputDocument) { if (attributesMap!=null) outputDocument.replace(element.getAttributes(),attributesMap); } } }