/*
* Copyright (c) 2002-2008 Gargoyle Software Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.gargoylesoftware.htmlunit.html;
import static com.gargoylesoftware.htmlunit.protocol.javascript.JavaScriptURLConnection.JAVASCRIPT_PREFIX;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.util.EncodingUtil;
import org.apache.commons.lang.StringUtils;
import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.ElementNotFoundException;
import com.gargoylesoftware.htmlunit.FormEncodingType;
import com.gargoylesoftware.htmlunit.HttpMethod;
import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.ScriptResult;
import com.gargoylesoftware.htmlunit.SgmlPage;
import com.gargoylesoftware.htmlunit.TextUtil;
import com.gargoylesoftware.htmlunit.WebAssert;
import com.gargoylesoftware.htmlunit.WebRequestSettings;
import com.gargoylesoftware.htmlunit.WebWindow;
import com.gargoylesoftware.htmlunit.javascript.host.Event;
/**
* Wrapper for the HTML element "form".
*
* @version $Revision$
* @author Mike Bowler
* @author David K. Taylor
* @author Brad Clarke
* @author Christian Sell
* @author Marc Guillemot
* @author George Murnock
* @author Kent Tong
* @author Ahmed Ashour
* @author Philip Graf
*/
public class HtmlForm extends ClickableElement {
private static final long serialVersionUID = 5338964478788825866L;
/** The HTML tag represented by this element. */
public static final String TAG_NAME = "form";
private static final Collection SUBMITTABLE_ELEMENT_NAMES =
Arrays.asList(new String[]{"input", "button", "select", "textarea", "isindex"});
private final List lostChildren_ = new ArrayList();
/**
* Creates an instance.
*
* @param namespaceURI the URI that identifies an XML namespace
* @param qualifiedName the qualified name of the element type to instantiate
* @param htmlPage the page that contains this element
* @param attributes the initial attributes
*/
HtmlForm(final String namespaceURI, final String qualifiedName, final SgmlPage htmlPage,
final Map attributes) {
super(namespaceURI, qualifiedName, htmlPage, attributes);
}
/**
* INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
*
* Submit this form to the appropriate server. If submitElement is null then
* treat this as if it was called by JavaScript. In this case, the onsubmit
* handler will not get executed.
*
* @param submitElement the element that caused the submit to occur
* @return a new page that reflects the results of this submission
* @exception IOException If an IO error occurs
*/
public Page submit(final SubmittableElement submitElement) throws IOException {
final HtmlPage htmlPage = (HtmlPage) getPage();
if (htmlPage.getWebClient().isJavaScriptEnabled()) {
if (submitElement != null) {
final ScriptResult scriptResult = fireEvent(Event.TYPE_SUBMIT);
if (ScriptResult.isFalse(scriptResult)) {
return scriptResult.getNewPage();
}
}
final String action = getActionAttribute();
if (TextUtil.startsWithIgnoreCase(action, JAVASCRIPT_PREFIX)) {
return htmlPage.executeJavaScriptIfPossible(action, "Form action", getStartLineNumber()).getNewPage();
}
}
else {
if (TextUtil.startsWithIgnoreCase(getActionAttribute(), JAVASCRIPT_PREFIX)) {
// The action is JavaScript but JavaScript isn't enabled.
// Return the current page.
return htmlPage;
}
}
final List parameters = getParameterListForSubmit(submitElement);
final HttpMethod method;
final String methodAttribute = getMethodAttribute();
if ("post".equalsIgnoreCase(methodAttribute)) {
method = HttpMethod.POST;
}
else {
if (!"get".equalsIgnoreCase(methodAttribute) && methodAttribute.trim().length() > 0) {
notifyIncorrectness("Incorrect submit method >" + getMethodAttribute() + "<. Using >GET<.");
}
method = HttpMethod.GET;
}
String actionUrl = getActionAttribute();
if (HttpMethod.GET == method) {
final String anchor = StringUtils.substringAfter(actionUrl, "#");
actionUrl = StringUtils.substringBefore(actionUrl, "#");
final NameValuePair[] pairs = new NameValuePair[parameters.size()];
parameters.toArray(pairs);
final String queryFromFields = EncodingUtil.formUrlEncode(pairs, getPage().getPageEncoding());
// action may already contain some query parameters: they have to be removed
actionUrl = StringUtils.substringBefore(actionUrl, "?");
final BrowserVersion browserVersion = getPage().getWebClient().getBrowserVersion();
if (!(browserVersion.isIE() && browserVersion.getBrowserVersionNumeric() >= 7)
|| queryFromFields.length() > 0) {
actionUrl += "?" + queryFromFields;
}
if (anchor.length() > 0) {
actionUrl += "#" + anchor;
}
parameters.clear(); // parameters have been added to query
}
final URL url;
try {
url = htmlPage.getFullyQualifiedUrl(actionUrl);
}
catch (final MalformedURLException e) {
throw new IllegalArgumentException("Not a valid url: " + actionUrl);
}
final WebRequestSettings settings = new WebRequestSettings(url, method);
settings.setRequestParameters(parameters);
settings.setEncodingType(FormEncodingType.getInstance(getEnctypeAttribute()));
settings.setCharset(getSubmitCharset());
settings.addAdditionalHeader("Referer", htmlPage.getWebResponse().getRequestUrl().toExternalForm());
final WebWindow webWindow = htmlPage.getEnclosingWindow();
return htmlPage.getWebClient().getPage(
webWindow,
htmlPage.getResolvedTarget(getTargetAttribute()),
settings);
}
/**
* Returns the charset to use for the form submission. This is the first one
* from the list provided in {@link #getAcceptCharsetAttribute()} if any
* or the page's charset else
* @return the charset to use for the form submission
*/
private String getSubmitCharset() {
if (getAcceptCharsetAttribute().length() > 0) {
return getAcceptCharsetAttribute().trim().replaceAll("[ ,].*", "");
}
return getPage().getPageEncoding();
}
/**
* Returns a list of {@link KeyValuePair}s that represent the data that will be
* sent to the server when this form is submitted. This is primarily intended to aid
* debugging.
*
* @param submitElement the element used to submit the form, or null if the
* form was submitted by JavaScript
* @return the list of {@link KeyValuePair}s that represent that data that will be sent
* to the server when this form is submitted
*/
private List getParameterListForSubmit(final SubmittableElement submitElement) {
final Collection submittableElements = getSubmittableElements(submitElement);
final List parameterList = new ArrayList(submittableElements.size());
for (final SubmittableElement element : submittableElements) {
for (final NameValuePair pair : element.getSubmitKeyValuePairs()) {
parameterList.add(pair);
}
}
return parameterList;
}
/**
* Resets this form to its initial values, returning the page contained by this form's window after the
* reset. Note that the returned page may or may not be the same as the original page, based on JavaScript
* event handlers, etc.
*
* @return the page contained by this form's window after the reset
*/
public Page reset() {
final SgmlPage htmlPage = getPage();
final ScriptResult scriptResult = fireEvent(Event.TYPE_RESET);
if (ScriptResult.isFalse(scriptResult)) {
return scriptResult.getNewPage();
}
for (final HtmlElement next : getAllHtmlChildElements()) {
if (next instanceof SubmittableElement) {
((SubmittableElement) next).reset();
}
}
return htmlPage;
}
/**
* Returns a collection of elements that represent all the "submittable" elements in this form,
* assuming that the specified element is used to submit the form.
*
* @param submitElement the element used to submit the form, or null if the
* form is submitted by JavaScript
* @return a collection of elements that represent all the "submittable" elements in this form
*/
Collection getSubmittableElements(final SubmittableElement submitElement) {
final List submittableElements = new ArrayList();
for (final HtmlElement element : getAllHtmlChildElements()) {
if (isSubmittable(element, submitElement)) {
submittableElements.add((SubmittableElement) element);
}
}
for (final HtmlElement element : lostChildren_) {
if (isSubmittable(element, submitElement)) {
submittableElements.add((SubmittableElement) element);
}
}
return submittableElements;
}
private boolean isValidForSubmission(final HtmlElement element, final SubmittableElement submitElement) {
final String tagName = element.getTagName();
if (!SUBMITTABLE_ELEMENT_NAMES.contains(tagName)) {
return false;
}
if (element.isAttributeDefined("disabled")) {
return false;
}
// clicked input type="image" is submitted even if it hasn't a name
if (element == submitElement && element instanceof HtmlImageInput) {
return true;
}
if (!tagName.equals("isindex") && !element.isAttributeDefined("name")) {
return false;
}
if (!tagName.equals("isindex") && element.getAttributeValue("name").equals("")) {
return false;
}
if (element instanceof HtmlInput) {
final String type = element.getAttributeValue("type").toLowerCase();
if (type.equals("radio") || type.equals("checkbox")) {
return element.isAttributeDefined("checked");
}
}
if (tagName.equals("select")) {
return ((HtmlSelect) element).isValidForSubmission();
}
return true;
}
/**
* Returns true if the specified element gets submitted when this form is submitted,
* assuming that the form is submitted using the specified submit element.
*
* @param element the element to check
* @param submitElement the element used to submit the form, or null if the form is
* submitted by JavaScript
* @return true if the specified element gets submitted when this form is submitted
*/
private boolean isSubmittable(final HtmlElement element, final SubmittableElement submitElement) {
final String tagName = element.getTagName();
if (!isValidForSubmission(element, submitElement)) {
return false;
}
// The one submit button that was clicked can be submitted but no other ones
if (element == submitElement) {
return true;
}
if (element instanceof HtmlInput) {
final HtmlInput input = (HtmlInput) element;
final String type = input.getTypeAttribute().toLowerCase();
if (type.equals("submit") || type.equals("image") || type.equals("reset") || type.equals("button")) {
return false;
}
}
if (tagName.equals("button")) {
return false;
}
return true;
}
/**
* Returns all input elements which are members of this form and have the specified name.
*
* @param name the input name to search for
* @return all input elements which are members of this form and have the specified name
*/
public List getInputsByName(final String name) {
final List list = getHtmlElementsByAttribute("input", "name", name);
// collect inputs from lost children
for (final HtmlElement elt : getLostChildren()) {
if (elt instanceof HtmlInput && name.equals(elt.getAttribute("name"))) {
list.add((HtmlInput) elt);
}
}
return list;
}
/**
* Returns the first input element which is a member of this form and has the specified name.
*
* @param name the input name to search for
* @param the input type
* @return the first input element which is a member of this form and has the specified name
* @throws ElementNotFoundException if there is not input in this form with the specified name
*/
@SuppressWarnings("unchecked")
public final I getInputByName(final String name) throws ElementNotFoundException {
final List inputs = getInputsByName(name);
if (inputs.isEmpty()) {
throw new ElementNotFoundException("input", "name", name);
}
return (I) inputs.get(0);
}
/**
* Returns all the {@link HtmlSelect} elements in this form that have the specified name.
*
* @param name the name to search for
* @return all the {@link HtmlSelect} elements in this form that have the specified name
*/
public List getSelectsByName(final String name) {
final List list = getHtmlElementsByAttribute("select", "name", name);
// collect selects from lost children
for (final HtmlElement elt : getLostChildren()) {
if (elt instanceof HtmlSelect && name.equals(elt.getAttribute("name"))) {
list.add((HtmlSelect) elt);
}
}
return list;
}
/**
* Returns the first {@link HtmlSelect} element in this form that has the specified name.
*
* @param name the name to search for
* @return the first {@link HtmlSelect} element in this form that has the specified name
* @throws ElementNotFoundException if this form does not contain a {@link HtmlSelect}
* element with the specified name
*/
public HtmlSelect getSelectByName(final String name) throws ElementNotFoundException {
final List list = getSelectsByName(name);
if (list.isEmpty()) {
throw new ElementNotFoundException("select", "name", name);
}
return list.get(0);
}
/**
* Returns all the {@link HtmlButton} elements in this form that have the specified name.
*
* @param name the name to search for
* @return all the {@link HtmlButton} elements in this form that have the specified name
*/
public List getButtonsByName(final String name) {
final List list = getHtmlElementsByAttribute("button", "name", name);
// collect buttons from lost children
for (final HtmlElement elt : getLostChildren()) {
if (elt instanceof HtmlButton && name.equals(elt.getAttribute("name"))) {
list.add((HtmlButton) elt);
}
}
return list;
}
/**
* Returns the first {@link HtmlButton} element in this form that has the specified name.
*
* @param name the name to search for
* @return the first {@link HtmlButton} element in this form that has the specified name
* @throws ElementNotFoundException if this form does not contain a {@link HtmlButton}
* element with the specified name
*/
public HtmlButton getButtonByName(final String name) throws ElementNotFoundException {
final List list = getButtonsByName(name);
if (list.isEmpty()) {
throw new ElementNotFoundException("button", "name", name);
}
return list.get(0);
}
/**
* Returns all the {@link HtmlTextArea} elements in this form that have the specified name.
*
* @param name the name to search for
* @return all the {@link HtmlTextArea} elements in this form that have the specified name
*/
public List getTextAreasByName(final String name) {
final List list = getHtmlElementsByAttribute("textarea", "name", name);
// collect buttons from lost children
for (final HtmlElement elt : getLostChildren()) {
if (elt instanceof HtmlTextArea && name.equals(elt.getAttribute("name"))) {
list.add((HtmlTextArea) elt);
}
}
return list;
}
/**
* Returns the first {@link HtmlTextArea} element in this form that has the specified name.
*
* @param name the name to search for
* @return the first {@link HtmlTextArea} element in this form that has the specified name
* @throws ElementNotFoundException if this form does not contain a {@link HtmlTextArea}
* element with the specified name
*/
public HtmlTextArea getTextAreaByName(final String name) throws ElementNotFoundException {
final List list = getTextAreasByName(name);
if (list.isEmpty()) {
throw new ElementNotFoundException("textarea", "name", name);
}
return list.get(0);
}
/**
* Returns all the {@link HtmlRadioButtonInput} elements in this form that have the specified name.
*
* @param name the name to search for
* @return all the {@link HtmlRadioButtonInput} elements in this form that have the specified name
*/
public List getRadioButtonsByName(final String name) {
WebAssert.notNull("name", name);
final List results = new ArrayList();
for (final HtmlElement element : getInputsByName(name)) {
if (element instanceof HtmlRadioButtonInput) {
results.add((HtmlRadioButtonInput) element);
}
}
return results;
}
/**
* Selects the specified radio button in the form. Only a radio button that is actually contained
* in the form can be selected.
*
* @param radioButtonInput the radio button to select
*/
void setCheckedRadioButton(final HtmlRadioButtonInput radioButtonInput) {
if (!isAncestorOf(radioButtonInput) && !lostChildren_.contains(radioButtonInput)) {
throw new IllegalArgumentException("HtmlRadioButtonInput is not child of this HtmlForm");
}
final List radios = getRadioButtonsByName(radioButtonInput.getNameAttribute());
for (final HtmlRadioButtonInput input : radios) {
if (input == radioButtonInput) {
input.setAttributeValue("checked", "checked");
}
else {
input.removeAttribute("checked");
}
}
}
/**
* Returns the first checked radio button with the specified name. If none of
* the radio buttons by that name are checked, this method returns null.
*
* @param name the name of the radio button
* @return the first checked radio button with the specified name
*/
public HtmlRadioButtonInput getCheckedRadioButton(final String name) {
WebAssert.notNull("name", name);
for (final HtmlRadioButtonInput input : getRadioButtonsByName(name)) {
if (input.isChecked()) {
return input;
}
}
return null;
}
/**
* Returns the value of the attribute "action". Refer to the HTML 4.01 documentation for
* details on the use of this attribute.
*
* @return the value of the attribute "action" or an empty string if that attribute isn't defined
*/
public final String getActionAttribute() {
return getAttributeValue("action");
}
/**
* Sets the value of the attribute "action". Refer to the HTML 4.01 documentation for
* details on the use of this attribute.
*
* @param action the value of the attribute "action"
*/
public final void setActionAttribute(final String action) {
setAttributeValue("action", action);
}
/**
* Returns the value of the attribute "method". Refer to the HTML 4.01 documentation for
* details on the use of this attribute.
*
* @return the value of the attribute "method" or an empty string if that attribute isn't defined
*/
public final String getMethodAttribute() {
return getAttributeValue("method");
}
/**
* Sets the value of the attribute "method". Refer to the HTML 4.01 documentation for
* details on the use of this attribute.
*
* @param method the value of the attribute "method"
*/
public final void setMethodAttribute(final String method) {
setAttributeValue("method", method);
}
/**
* Returns the value of the attribute "name". Refer to the HTML 4.01 documentation for
* details on the use of this attribute.
*
* @return the value of the attribute "name" or an empty string if that attribute isn't defined
*/
public final String getNameAttribute() {
return getAttributeValue("name");
}
/**
* Sets the value of the attribute "name". Refer to the HTML 4.01 documentation for
* details on the use of this attribute.
*
* @param name the value of the attribute "name"
*/
public final void setNameAttribute(final String name) {
setAttributeValue("name", name);
}
/**
* Returns the value of the attribute "enctype". Refer to the HTML 4.01 documentation for
* details on the use of this attribute. "Enctype" is the encoding type
* used when submitting a form back to the server.
*
* @return the value of the attribute "enctype" or an empty string if that attribute isn't defined
*/
public final String getEnctypeAttribute() {
return getAttributeValue("enctype");
}
/**
* Sets the value of the attribute "enctype". Refer to the HTML 4.01 documentation for
* details on the use of this attribute. "Enctype" is the encoding type
* used when submitting a form back to the server.
*
* @param encoding the value of the attribute "enctype"
*/
public final void setEnctypeAttribute(final String encoding) {
setAttributeValue("enctype", encoding);
}
/**
* Returns the value of the attribute "onsubmit". Refer to the HTML 4.01 documentation for
* details on the use of this attribute.
*
* @return the value of the attribute "onsubmit" or an empty string if that attribute isn't defined
*/
public final String getOnSubmitAttribute() {
return getAttributeValue("onsubmit");
}
/**
* Returns the value of the attribute "onreset". Refer to the HTML 4.01 documentation for
* details on the use of this attribute.
*
* @return the value of the attribute "onreset" or an empty string if that attribute isn't defined
*/
public final String getOnResetAttribute() {
return getAttributeValue("onreset");
}
/**
* Returns the value of the attribute "accept". Refer to the HTML 4.01 documentation for
* details on the use of this attribute.
*
* @return the value of the attribute "accept" or an empty string if that attribute isn't defined
*/
public final String getAcceptAttribute() {
return getAttributeValue("accept");
}
/**
* Returns the value of the attribute "accept-charset". Refer to the
* HTML 4.01 documentation for details on the use of this attribute.
*
* @return the value of the attribute "accept-charset" or an empty string if that attribute isn't defined
*/
public final String getAcceptCharsetAttribute() {
return getAttributeValue("accept-charset");
}
/**
* Returns the value of the attribute "target". Refer to the HTML 4.01 documentation for
* details on the use of this attribute.
*
* @return the value of the attribute "target" or an empty string if that attribute isn't defined
*/
public final String getTargetAttribute() {
return getAttributeValue("target");
}
/**
* Sets the value of the attribute "target". Refer to the HTML 4.01 documentation for
* details on the use of this attribute.
*
* @param target the value of the attribute "target"
*/
public final void setTargetAttribute(final String target) {
setAttributeValue("target", target);
}
/**
* Returns the first input in this form with the specified value.
* @param value the value to search for
* @param the input type
* @return the first input in this form with the specified value
* @throws ElementNotFoundException if this form does not contain any inputs with the specified value
*/
@SuppressWarnings("unchecked")
public I getInputByValue(final String value) throws ElementNotFoundException {
final List list = getInputsByValue(value);
if (list.isEmpty()) {
throw new ElementNotFoundException("input", "value", value);
}
return (I) list.get(0);
}
/**
* Returns all the inputs in this form with the specified value.
* @param value the value to search for
* @return all the inputs in this form with the specified value
*/
public List getInputsByValue(final String value) {
final List results = getHtmlElementsByAttribute("input", "value", value);
for (final HtmlElement element : getLostChildren()) {
if (element instanceof HtmlInput && value.equals(element.getAttribute("value"))) {
results.add((HtmlInput) element);
}
}
return results;
}
/**
* Allows the parser to notify the form of a field that doesn't belong to its DOM children
* due to malformed HTML code
* @param element the form field
*/
void addLostChild(final HtmlElement field) {
lostChildren_.add(field);
field.setOwningForm(this);
}
/**
* Gets the form elements that may be submitted but that don't belong to the form's children
* in the DOM due to incorrect html code.
* @return the elements
*/
public List getLostChildren() {
return lostChildren_;
}
}