au.id.jericho.lib.html.FormField Maven / Gradle / Ivy
// Jericho HTML Parser - Java based library for analysing and manipulating HTML
// Version 2.6
// Copyright (C) 2007 Martin Jericho
// http://jerichohtml.sourceforge.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 au.id.jericho.lib.html;
import java.util.*;
/**
* Represents a field in an HTML form,
* a field being defined as the group of all {@linkplain FormControl form controls}
* having the same {@linkplain FormControl#getName() name}.
*
* The {@link #getFormControls()} method can be used to obtain the collection of this field's constituent
* {@link FormControl} objects.
*
* The {@link FormFields} class, which represents a collection of FormField
objects, provides the highest level
* interface for dealing with form fields and controls. For the most common tasks it can be used directly without
* the need to work with its constituent FormField
or {@link FormControl} objects.
*
* The FormField
class serves two main purposes:
*
* -
* Provide methods for the modification and retrieval of form control submission values
* while ensuring that the states of all the field's constituent form controls remain consistent with each other.
*
* The methods available for this purpose are:
* {@link #getValues() List getValues()}
* {@link #clearValues() void clearValues()}
* {@link #setValues(Collection) void setValues(Collection)}
* {@link #setValue(CharSequence) boolean setValue(CharSequence)}
* {@link #addValue(CharSequence) boolean addValue(CharSequence)}
*
* Although the {@link FormControl} class provides methods for directly modifying the submission values
* of individual form controls, it is generally recommended to use the interface provided by the {@link FormFields} class
* unless there is a specific requirement for the lower level functionality.
* The {@link FormFields} class contains convenience methods providing most of the functionality of the above methods,
* as well as some higher level functionality such as the ability to set the form
* submission values as a complete field data set
* using the {@link FormFields#setDataSet(Map)} method.
*
-
* Provide a means of determining the data structure of the field, allowing a server receiving a
* submitted
* form data set
* to interpret and store the data in an appropriate way.
*
* The properties available for this purpose are:
* {@link #allowsMultipleValues() boolean allowsMultipleValues()}
* {@link #getUserValueCount() int getUserValueCount()}
* {@link #getPredefinedValues() Collection getPredefinedValues()}
*
* The {@link FormFields#getColumnLabels()} and {@link FormFields#getColumnValues(Map)} methods utilise these properties
* to convert data from a form data set
* (represented as a field data set) into a simple array format,
* suitable for storage in a tabular format such as a database table or .CSV
file.
*
* The properties need only be utilised directly in the event that a
* form data set is to be converted
* from its normal format into some other type of data structure.
*
* A form field which allows user values normally consists of a single
* user value control,
* such as a {@link FormControlType#TEXT TEXT} control.
*
* When a form field consists of more than one control, these controls are normally all
* predefined value controls of the same
* {@linkplain FormControlType type}, such as {@link FormControlType#CHECKBOX CHECKBOX} controls.
*
* Form fields consisting of more than one control do not necessarily return {@linkplain #allowsMultipleValues() multiple values}.
* A form field consisting of {@link FormControlType#CHECKBOX CHECKBOX} controls can return multiple values, whereas
* a form field consisting of {@link FormControlType#CHECKBOX RADIO} controls returns at most one value.
*
* The HTML author can disregard convention and mix all types of controls with the same name in the same form,
* or include multiple user value controls of the same name.
* The evidence that such an unusual combination is present is when {@link #getUserValueCount()}>1
.
*
* FormField
instances are created automatically with the creation of a {@link FormFields} collection.
*
* The case sensitivity of form field names is determined by the static
* {@link Config#CurrentCompatibilityMode}.
{@link Config.CompatibilityMode#isFormFieldNameCaseInsensitive() FormFieldNameCaseInsensitive} property.
*
* @see FormFields
* @see FormControl
* @see FormControlType
*/
public final class FormField {
private final String name;
private int userValueCount=0;
private boolean allowsMultipleValues=false;
private LinkedHashSet predefinedValues=null; // String objects, null if none
private final LinkedHashSet formControls=new LinkedHashSet();
private transient FormControl firstFormControl=null; // this field is simply a cache for the getFirstFormControl() method
int columnIndex; // see FormFields.initColumns()
/** Constructor called from FormFields class. */
FormField(final String name) {
this.name=name;
}
/**
* Returns the control name shared by all of this field's constituent {@linkplain FormControl controls}.
*
* If the static {@link Config#CurrentCompatibilityMode}.
{@link Config.CompatibilityMode#isFormFieldNameCaseInsensitive() isFormFieldNameCaseInsensitive()}
* property is set to true
, the grouping of the controls by name is case insensitive
* and this method always returns the name in lower case.
*
* Since a form field is simply a group of controls with the same name, the terms control name and
* field name are for the most part synonymous, with only a possible difference in case differentiating them.
*
* @return the control name shared by all of this field's constituent {@linkplain FormControl controls}.
* @see FormControl#getName()
*/
public String getName() {
return name;
}
/**
* Returns a collection of all the constituent {@linkplain FormControl form controls} in this field.
*
* An iterator over this collection returns the controls in the order of appearance in the source.
*
* @return a collection of all the constituent {@linkplain FormControl form controls} in this field.
* @see #getFormControl()
* @see #getFormControl(String predefinedValue)
*/
public Collection getFormControls() {
return formControls;
}
/**
* Returns the constituent {@link FormControl} with the specified {@linkplain FormControl#getPredefinedValue() predefined value}.
*
* Specifying a predefined value of null
returns the first control without a predefined value.
*
* @param predefinedValue the predefined value of the control to be returned, or null
to return the first control without a predefined value.
* @return the constituent {@link FormControl} with the specified {@linkplain FormControl#getPredefinedValue() predefined value}, or null
if none exists.
* @see #getFormControl()
* @see #getFormControls()
*/
public FormControl getFormControl(final String predefinedValue) {
if (predefinedValue==null) {
for (final Iterator i=formControls.iterator(); i.hasNext();) {
final FormControl formControl=(FormControl)i.next();
if (!formControl.getFormControlType().hasPredefinedValue()) return formControl;
if (formControl.getFormControlType().getElementName()!=Tag.SELECT && formControl.getPredefinedValue()==null) return formControl;
}
} else {
for (final Iterator i=formControls.iterator(); i.hasNext();) {
final FormControl formControl=(FormControl)i.next();
if (formControl.getFormControlType().getElementName()==Tag.SELECT) {
if (formControl.getPredefinedValues().contains(predefinedValue)) return formControl;
} else {
if (predefinedValue.equals(formControl.getPredefinedValue())) return formControl;
}
}
}
return null;
}
/**
* Returns the first {@link FormControl} from this field.
* @return the first {@link FormControl} from this field, guaranteed not null
.
* @see #getFormControl(String predefinedValue)
* @see #getFormControls()
*/
public FormControl getFormControl() {
return (FormControl)formControls.iterator().next();
}
/**
* Indicates whether the field allows multiple values.
*
* Returns false
in any one of the following circumstances:
*
* - The field consists of only one control (unless it is a
* {@linkplain FormControlType#SELECT_MULTIPLE multiple select} with more than one option)
*
- The field consists entirely of {@linkplain FormControlType#RADIO radio buttons}
*
- The field consists entirely of {@linkplain FormControlType#isSubmit() submit} buttons
*
* If none of these three conditions are met, the method returns true
.
*
* @return true
if the field allows multiple values, otherwise false
.
*/
public boolean allowsMultipleValues() {
return allowsMultipleValues;
}
/**
* Returns the number of constituent user value controls in this field.
* This should in most cases be either 0
or 1
.
*
* A value of 0
indicates the field values consist only of
* {@linkplain #getPredefinedValues() predefined values}, which is the case when the field consists only of
* predefined value controls.
*
* A value of 1
indicates the field values consist of at most one value set by the user.
* It is still possible in this case to receive multiple values in the unlikely event that the HTML author mixed
* controls of different types with the same name, but any other values would consist only of
* {@linkplain #getPredefinedValues() predefined values}.
*
* A value greater than 1
indicates that the HTML author has included more than one
* user value control with the same name.
* This would nearly always indicate an unintentional error in the HTML source document,
* in which case your application can either log a warning that a poorly designed form has been encountered,
* or take special action to try to interpret the multiple user values that might be submitted.
*
* @return the number of constituent user value controls in this field.
*/
public int getUserValueCount() {
return userValueCount;
}
/**
* Returns a collection of the {@linkplain FormControl#getPredefinedValue() predefined values} of all constituent {@linkplain FormControl controls} in this field.
*
* All objects in the returned collection are of type String
, with no null
entries.
*
* An interator over this collection returns the values in the order of appearance in the source document.
*
* @return a collection of the {@linkplain FormControl#getPredefinedValue() predefined values} of all constituent {@linkplain FormControl controls} in this field, or null
if none.
* @see FormControl#getPredefinedValues()
*/
public Collection getPredefinedValues() {
return predefinedValues!=null ? predefinedValues : Collections.EMPTY_SET;
}
/**
* Returns a list of the field submission values in order of appearance.
*
* The term field submission values is used in this library to refer to the aggregate of all the
* submission values of a field's constituent {@linkplain #getFormControls() form controls}.
*
* All objects in the returned list are of type CharSequence
, with no null
entries.
*
* The list may contain duplicates if the this field has multiple controls with the same value.
*
* @return a list of the field submission values in order of appearance, guaranteed not null
.
*/
public List getValues() {
final List values=new ArrayList();
for (final Iterator i=formControls.iterator(); i.hasNext();)
((FormControl)i.next()).addValuesTo(values);
return values;
}
/**
* Clears the submission values of all the constituent {@linkplain #getFormControls() form controls} in this field.
* @see FormControl#clearValues()
*/
public void clearValues() {
for (final Iterator i=formControls.iterator(); i.hasNext();)
((FormControl)i.next()).clearValues();
}
/**
* Sets the field submission values of this field to the specified values.
*
* This is equivalent to calling {@link #clearValues()} followed by {@link #addValue(CharSequence) addValue(value)} for each
* value in the specified collection.
*
* The specified collection must not contain any null
values.
*
* @param values the new field submission values of this field.
* @see #addValue(CharSequence value)
*/
public void setValues(final Collection values) {
clearValues();
addValues(values);
}
/**
* Sets the field submission values of this field to the single specified value.
*
* This is equivalent to calling {@link #clearValues()} followed by {@link #addValue(CharSequence) addValue(value)}.
*
* The return value indicates whether any of the constituent form controls "accepted" the value.
* A return value of false
implies an error condition as the specified value is not compatible with this field.
*
* Specifying a null
value is equivalent to calling {@link #clearValues()} alone, and always returns true
.
*
* See the {@link #addValue(CharSequence value)} method for more information.
*
* @param value the new field submission value of this field, or null
to {@linkplain #clearValues() clear} the field of all submission values.
* @return true
if one of the constituent {@linkplain #getFormControls() form controls} accepts the value, otherwise false
.
* @see FormFields#setValue(String fieldName, CharSequence value)
*/
public boolean setValue(final CharSequence value) {
clearValues();
return value!=null ? addValue(value) : true;
}
/**
* Adds the specified value to the field submission values of this field.
*
* This is achieved internally by attempting to {@linkplain FormControl#addValue(CharSequence) add the value} to every constituent
* {@linkplain #getFormControls() form control} until one "accepts" it.
*
* The return value indicates whether any of the constituent form controls accepted the value.
* A return value of false
implies an error condition as the specified value is not compatible with this field.
*
* In the unusual case that this field consists of multiple form controls, but not all of them are
* predefined value controls, priority is given to the predefined value controls
* before attempting to add the value to the user value controls.
*
* @param value the new field submission value to add to this field, must not be null
.
* @return true
if one of the constituent {@linkplain #getFormControls() form controls} accepts the value, otherwise false
.
*/
public boolean addValue(final CharSequence value) {
if (value==null) throw new IllegalArgumentException("value argument must not be null");
if (formControls.size()==1) return getFirstFormControl().addValue(value);
List userValueControls=null;
for (final Iterator i=formControls.iterator(); i.hasNext();) {
final FormControl formControl=(FormControl)i.next();
if (!formControl.getFormControlType().hasPredefinedValue()) {
// A user value control has been found, but is not the only control with this name.
// This shouldn't normally happen in a well designed form, but we will save the user value control
// for later and give all predefined value controls first opportunity to take the value.
if (userValueControls==null) userValueControls=new LinkedList();
userValueControls.add(formControl);
continue;
}
if (formControl.addValue(value)) return true; // return value of true from formControl.addValue(value) means the value was taken by the control
}
if (userValueControls==null) return false;
for (final Iterator i=userValueControls.iterator(); i.hasNext();) {
final FormControl formControl=(FormControl)i.next();
if (formControl.addValue(value)) return true;
}
return false;
}
/**
* Returns a string representation of this object useful for debugging purposes.
* @return a string representation of this object useful for debugging purposes.
*/
public String getDebugInfo() {
final StringBuffer sb=new StringBuffer();
sb.append("Field: ").append(name).append(", UserValueCount=").append(userValueCount).append(", AllowsMultipleValues=").append(allowsMultipleValues);
if (predefinedValues!=null) {
for (final Iterator i=predefinedValues.iterator(); i.hasNext();) {
sb.append(Config.NewLine).append("PredefinedValue: ");
sb.append(i.next());
}
}
for (final Iterator i=formControls.iterator(); i.hasNext();) {
sb.append(Config.NewLine).append("FormControl: ");
sb.append(((FormControl)i.next()).getDebugInfo());
}
sb.append(Config.NewLine).append(Config.NewLine);
return sb.toString();
}
/**
* Returns a string representation of this object useful for debugging purposes.
*
* This is equivalent to {@link #getDebugInfo()}.
*
* @return a string representation of this object useful for debugging purposes.
*/
public String toString() {
return getDebugInfo();
}
void addValues(final Collection values) {
if (values!=null) for (final Iterator i=values.iterator(); i.hasNext();) addValue((CharSequence)i.next());
}
void addValues(final CharSequence[] values) {
if (values!=null) for (int i=0; i1) return true;
if (userValueCount==1) return predefinedValues!=null;
// at this stage we know userValueCount==0 && predefinedValues.size()>=1
if (predefinedValues.size()==1) return false;
final FormControlType newFormControlType=newFormControl.getFormControlType();
if (formControls.size()==1) return newFormControlType==FormControlType.SELECT_MULTIPLE;
// at this stage we know there are multiple predefined values in multiple controls.
// if all of the controls are radio buttons or all are submit buttons, allowsMultipleValues is false, otherwise true.
// checking only the first control and the new control is equivalent to checking them all because if they weren't all
// the same allowsMultipleValues would already be true.
final FormControlType firstFormControlType=getFirstFormControl().getFormControlType();
if (newFormControlType==FormControlType.RADIO && firstFormControlType==FormControlType.RADIO) return false;
if (newFormControlType.isSubmit() && firstFormControlType.isSubmit()) return false;
return true;
}
FormControl getFirstFormControl() {
// formControls must be ordered collection for this method to work.
// It has to return the first FormControl entered into the collection
// for the algorithm in calculateAllowsMultipleValues() to work.
if (firstFormControl==null) firstFormControl=(FormControl)formControls.iterator().next();
return firstFormControl;
}
/** only called from FormFields class */
void merge(final FormField formField) {
if (formField.userValueCount>userValueCount) userValueCount=formField.userValueCount;
allowsMultipleValues=allowsMultipleValues || formField.allowsMultipleValues;
if (predefinedValues==null) {
predefinedValues=formField.predefinedValues;
} else if (formField.predefinedValues!=null) {
for (final Iterator i=formField.predefinedValues.iterator(); i.hasNext();)
predefinedValues.add(i.next());
}
for (final Iterator i=formField.getFormControls().iterator(); i.hasNext();)
formControls.add(i.next());
}
}