001package org.jsoup.nodes;
002
003import org.jsoup.Connection;
004import org.jsoup.Jsoup;
005import org.jsoup.helper.HttpConnection;
006import org.jsoup.helper.Validate;
007import org.jsoup.parser.Tag;
008import org.jsoup.select.Elements;
009
010import java.util.ArrayList;
011import java.util.List;
012
013/**
014 * A HTML Form Element provides ready access to the form fields/controls that are associated with it. It also allows a
015 * form to easily be submitted.
016 */
017public class FormElement extends Element {
018    private final Elements elements = new Elements();
019
020    /**
021     * Create a new, standalone form element.
022     *
023     * @param tag        tag of this element
024     * @param baseUri    the base URI
025     * @param attributes initial attributes
026     */
027    public FormElement(Tag tag, String baseUri, Attributes attributes) {
028        super(tag, baseUri, attributes);
029    }
030
031    /**
032     * Get the list of form control elements associated with this form.
033     * @return form controls associated with this element.
034     */
035    public Elements elements() {
036        return elements;
037    }
038
039    /**
040     * Add a form control element to this form.
041     * @param element form control to add
042     * @return this form element, for chaining
043     */
044    public FormElement addElement(Element element) {
045        elements.add(element);
046        return this;
047    }
048
049    @Override
050    protected void removeChild(Node out) {
051        super.removeChild(out);
052        elements.remove(out);
053    }
054
055    /**
056     Prepare to submit this form. A Connection object is created with the request set up from the form values. This
057     Connection will inherit the settings and the cookies (etc) of the connection/session used to request this Document
058     (if any), as available in {@link Document#connection()}
059     <p>You can then set up other options (like user-agent, timeout, cookies), then execute it.</p>
060
061     @return a connection prepared from the values of this form, in the same session as the one used to request it
062     @throws IllegalArgumentException if the form's absolute action URL cannot be determined. Make sure you pass the
063     document's base URI when parsing.
064     */
065    public Connection submit() {
066        String action = hasAttr("action") ? absUrl("action") : baseUri();
067        Validate.notEmpty(action, "Could not determine a form action URL for submit. Ensure you set a base URI when parsing.");
068        Connection.Method method = attr("method").equalsIgnoreCase("POST") ?
069                Connection.Method.POST : Connection.Method.GET;
070
071        Document owner = ownerDocument();
072        Connection connection = owner != null? owner.connection().newRequest() : Jsoup.newSession();
073        return connection.url(action)
074                .data(formData())
075                .method(method);
076    }
077
078    /**
079     * Get the data that this form submits. The returned list is a copy of the data, and changes to the contents of the
080     * list will not be reflected in the DOM.
081     * @return a list of key vals
082     */
083    public List<Connection.KeyVal> formData() {
084        ArrayList<Connection.KeyVal> data = new ArrayList<>();
085
086        // iterate the form control elements and accumulate their values
087        for (Element el: elements) {
088            if (!el.tag().isFormSubmittable()) continue; // contents are form listable, superset of submitable
089            if (el.hasAttr("disabled")) continue; // skip disabled form inputs
090            String name = el.attr("name");
091            if (name.length() == 0) continue;
092            String type = el.attr("type");
093
094            if (type.equalsIgnoreCase("button") || type.equalsIgnoreCase("image")) continue; // browsers don't submit these
095
096            if (el.nameIs("select")) {
097                Elements options = el.select("option[selected]");
098                boolean set = false;
099                for (Element option: options) {
100                    data.add(HttpConnection.KeyVal.create(name, option.val()));
101                    set = true;
102                }
103                if (!set) {
104                    Element option = el.selectFirst("option");
105                    if (option != null)
106                        data.add(HttpConnection.KeyVal.create(name, option.val()));
107                }
108            } else if ("checkbox".equalsIgnoreCase(type) || "radio".equalsIgnoreCase(type)) {
109                // only add checkbox or radio if they have the checked attribute
110                if (el.hasAttr("checked")) {
111                    final String val = el.val().length() >  0 ? el.val() : "on";
112                    data.add(HttpConnection.KeyVal.create(name, val));
113                }
114            } else {
115                data.add(HttpConnection.KeyVal.create(name, el.val()));
116            }
117        }
118        return data;
119    }
120
121    @Override
122    public FormElement clone() {
123        return (FormElement) super.clone();
124    }
125}