/* * 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_; } }