001: /*
002: * Copyright (c) 2002-2008 Gargoyle Software Inc. All rights reserved.
003: *
004: * Redistribution and use in source and binary forms, with or without
005: * modification, are permitted provided that the following conditions are met:
006: *
007: * 1. Redistributions of source code must retain the above copyright notice,
008: * this list of conditions and the following disclaimer.
009: * 2. Redistributions in binary form must reproduce the above copyright notice,
010: * this list of conditions and the following disclaimer in the documentation
011: * and/or other materials provided with the distribution.
012: * 3. The end-user documentation included with the redistribution, if any, must
013: * include the following acknowledgment:
014: *
015: * "This product includes software developed by Gargoyle Software Inc.
016: * (http://www.GargoyleSoftware.com/)."
017: *
018: * Alternately, this acknowledgment may appear in the software itself, if
019: * and wherever such third-party acknowledgments normally appear.
020: * 4. The name "Gargoyle Software" must not be used to endorse or promote
021: * products derived from this software without prior written permission.
022: * For written permission, please contact info@GargoyleSoftware.com.
023: * 5. Products derived from this software may not be called "HtmlUnit", nor may
024: * "HtmlUnit" appear in their name, without prior written permission of
025: * Gargoyle Software Inc.
026: *
027: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
028: * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
029: * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GARGOYLE
030: * SOFTWARE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
031: * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
032: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
033: * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
034: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
035: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
036: * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
037: */
038: package com.gargoylesoftware.htmlunit.html;
039:
040: import java.util.ArrayList;
041: import java.util.Collections;
042: import java.util.Iterator;
043: import java.util.List;
044: import java.util.Map;
045:
046: import org.apache.commons.lang.ArrayUtils;
047:
048: import com.gargoylesoftware.htmlunit.Assert;
049: import com.gargoylesoftware.htmlunit.ElementNotFoundException;
050: import com.gargoylesoftware.htmlunit.KeyValuePair;
051: import com.gargoylesoftware.htmlunit.Page;
052:
053: /**
054: * Wrapper for the HTML element "select".
055: *
056: * @version $Revision: 2132 $
057: * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
058: * @author <a href="mailto:gudujarlson@sf.net">Mike J. Bresnahan</a>
059: * @author David K. Taylor
060: * @author <a href="mailto:cse@dynabean.de">Christian Sell</a>
061: * @author David D. Kilzer
062: * @author Marc Guillemot
063: * @author Daniel Gredler
064: * @author Ahmed Ashour
065: */
066: public class HtmlSelect extends FocusableElement implements
067: DisabledElement, SubmittableElement {
068:
069: private static final long serialVersionUID = 7893240015923163203L;
070:
071: /** the HTML tag represented by this element */
072: public static final String TAG_NAME = "select";
073:
074: /** @deprecated */
075: private String[] fakeSelectedValues_;
076:
077: /**
078: * Create an instance
079: *
080: * @param page The page that contains this element
081: * @param attributes the initial attributes
082: * @deprecated You should not directly construct HtmlSelect.
083: */
084: //TODO: to be removed, deprecated after 1.11
085: public HtmlSelect(final HtmlPage page, final Map attributes) {
086: this (null, TAG_NAME, page, attributes);
087: }
088:
089: /**
090: * Create an instance
091: *
092: * @param namespaceURI the URI that identifies an XML namespace.
093: * @param qualifiedName The qualified name of the element type to instantiate
094: * @param page The page that contains this element
095: * @param attributes the initial attributes
096: */
097: HtmlSelect(final String namespaceURI, final String qualifiedName,
098: final HtmlPage page, final Map attributes) {
099: super (namespaceURI, qualifiedName, page, attributes);
100: }
101:
102: /**
103: * If we were given an invalid <tt>size</tt> attribute, normalize it.
104: * Then set a default selected option if none was specified and the size is 1 or less
105: * and this isn't a multiple selection input.
106: */
107: protected void onAllChildrenAddedToPage() {
108:
109: // Fix the size if necessary.
110: int size;
111: try {
112: size = Integer.parseInt(getSizeAttribute());
113: if (size < 0) {
114: removeAttribute("size");
115: size = 0;
116: }
117: } catch (final NumberFormatException e) {
118: removeAttribute("size");
119: size = 0;
120: }
121:
122: // Set a default selected option if necessary.
123: if (getSelectedOptions().isEmpty() && size <= 1
124: && !isMultipleSelectEnabled()) {
125: final List options = getOptions();
126: if (!options.isEmpty()) {
127: final HtmlOption first = (HtmlOption) options.get(0);
128: first.setSelectedInternal(true);
129: }
130: }
131: }
132:
133: /**
134: * Return a List containing all of the currently selected options. The following special
135: * conditions can occur if the element is in single select mode:
136: * <ul>
137: * <li>if multiple options are erroneously selected, the last one is returned</li>
138: * <li>if no options are selected, the first one is returned</li>
139: * </ul>
140: *
141: * @return See above
142: */
143: public List getSelectedOptions() {
144: List result;
145: if (isMultipleSelectEnabled()) {
146: // Multiple selections possible.
147: result = new ArrayList();
148: final DescendantElementsIterator iterator = new DescendantElementsIterator();
149: while (iterator.hasNext()) {
150: final HtmlElement element = iterator.nextElement();
151: if (element instanceof HtmlOption
152: && ((HtmlOption) element).isSelected()) {
153: result.add(element);
154: }
155: }
156: } else {
157: // Only a single selection is possible.
158: result = new ArrayList(1);
159: HtmlOption lastSelected = null;
160: final DescendantElementsIterator iterator = new DescendantElementsIterator();
161: while (iterator.hasNext()) {
162: final HtmlElement element = iterator.nextElement();
163: if (element instanceof HtmlOption) {
164: final HtmlOption option = (HtmlOption) element;
165: if (option.isSelected()) {
166: lastSelected = option;
167: }
168: }
169: }
170: if (lastSelected != null) {
171: result.add(lastSelected);
172: }
173: }
174: return Collections.unmodifiableList(result);
175: }
176:
177: /**
178: * Return a List containing all the options
179: *
180: * @return See above
181: */
182: public List getOptions() {
183: final List elementList = getHtmlElementsByTagName("option");
184: return Collections.unmodifiableList(elementList);
185: }
186:
187: /**
188: * Return the indexed option.
189: *
190: * @param index The index
191: * @return The option specified by the index
192: */
193: public HtmlOption getOption(final int index) {
194: final List elementList = getHtmlElementsByTagName("option");
195: return (HtmlOption) elementList.get(index);
196: }
197:
198: /**
199: * Return the number of options
200: * @return The number of options
201: */
202: public int getOptionSize() {
203: final List elementList = getHtmlElementsByTagName("option");
204: return elementList.size();
205: }
206:
207: /**
208: * Remove options by reducing the "length" property. This has no
209: * effect if the length is set to the same or greater.
210: * @param newLength The new length property value
211: */
212: public void setOptionSize(final int newLength) {
213: final List elementList = getHtmlElementsByTagName("option");
214:
215: for (int i = elementList.size() - 1; i >= newLength; i--) {
216: ((HtmlElement) elementList.get(i)).remove();
217: }
218: }
219:
220: /**
221: * Remove an option at the given index.
222: * @param index The index of the option to remove
223: */
224: public void removeOption(final int index) {
225: final ChildElementsIterator iterator = new ChildElementsIterator();
226: for (int i = 0; iterator.hasNext(); i++) {
227: final HtmlElement element = iterator.nextElement();
228: if (i == index) {
229: element.remove();
230: return;
231: }
232: }
233: }
234:
235: /**
236: * Replace an option at the given index with a new option.
237: * @param index The index of the option to remove
238: * @param newOption The new option to replace to indexed option
239: */
240: public void replaceOption(final int index,
241: final HtmlOption newOption) {
242: final ChildElementsIterator iterator = new ChildElementsIterator();
243: for (int i = 0; iterator.hasNext(); i++) {
244: final HtmlElement element = iterator.nextElement();
245: if (i == index) {
246: element.replace(newOption);
247: return;
248: }
249: }
250:
251: if (newOption.isSelected()) {
252: setSelectedAttribute(newOption, true);
253: }
254: }
255:
256: /**
257: * Add a new option at the end.
258: * @param newOption The new option to add
259: */
260: public void appendOption(final HtmlOption newOption) {
261: appendDomChild(newOption);
262: }
263:
264: /**
265: * {@inheritDoc}
266: * @see DomNode#appendDomChild(DomNode)
267: */
268: public DomNode appendDomChild(final DomNode node) {
269: final DomNode response = super .appendDomChild(node);
270: if (node instanceof HtmlOption) {
271: final HtmlOption option = (HtmlOption) node;
272: if (option.isSelected()) {
273: setSelectedAttribute(option, true);
274: }
275: }
276: return response;
277: }
278:
279: /**
280: * Set the "selected" state of the specified option. If this "select" is
281: * single select then calling this will deselect all other options <p>
282: *
283: * Only options that are actually in the document may be selected. If you
284: * need to select an option that really isn't there (ie testing error
285: * cases) then use {@link #fakeSelectedAttribute(String)} or {@link
286: * #fakeSelectedAttribute(String[])} instead.
287: *
288: * @param isSelected true if the option is to become selected
289: * @param optionValue The value of the option that is to change
290: * @return The page that occupies this window after this change is made. It
291: * may be the same window or it may be a freshly loaded one.
292: */
293: public Page setSelectedAttribute(final String optionValue,
294: final boolean isSelected) {
295: try {
296: return setSelectedAttribute(getOptionByValue(optionValue),
297: isSelected);
298: } catch (final ElementNotFoundException e) {
299: throw new IllegalArgumentException(
300: "No option found with value: " + optionValue);
301: }
302: }
303:
304: /**
305: * Set the "selected" state of the specified option. If this "select" is
306: * single select then calling this will deselect all other options <p>
307: *
308: * Only options that are actually in the document may be selected. If you
309: * need to select an option that really isn't there (ie testing error
310: * cases) then use {@link #fakeSelectedAttribute(String)} or {@link
311: * #fakeSelectedAttribute(String[])} instead.
312: *
313: * @param isSelected true if the option is to become selected
314: * @param selectedOption The value of the option that is to change
315: * @return The page that occupies this window after this change is made. It
316: * may be the same window or it may be a freshly loaded one.
317: */
318: public Page setSelectedAttribute(final HtmlOption selectedOption,
319: final boolean isSelected) {
320: final boolean triggerHandler = (selectedOption.isSelected() != isSelected);
321:
322: fakeSelectedValues_ = null;
323:
324: // caution the HtmlOption may have been created from js and therefore the select now need
325: // to "know" that it is selected
326: if (isMultipleSelectEnabled()) {
327: selectedOption.setSelectedInternal(isSelected);
328: } else {
329: final Iterator iterator = getOptions().iterator();
330: while (iterator.hasNext()) {
331: final HtmlOption option = (HtmlOption) iterator.next();
332: option.setSelectedInternal(option == selectedOption
333: && isSelected);
334: }
335: }
336:
337: if (triggerHandler) {
338: return HtmlInput.executeOnChangeHandlerIfAppropriate(this );
339: } else {
340: // nothing to do
341: return getPage();
342: }
343: }
344:
345: /**
346: * Set the selected value to be something that was not originally contained in the document.
347: *
348: * @param optionValue The value of the new "selected" option
349: * @deprecated
350: */
351: public void fakeSelectedAttribute(final String optionValue) {
352: Assert.notNull("optionValue", optionValue);
353: fakeSelectedAttribute(new String[] { optionValue });
354: }
355:
356: /**
357: * Set the selected values to be something that were not originally contained in the document.
358: *
359: * @param optionValues The values of the new "selected" options
360: * @deprecated
361: */
362: public void fakeSelectedAttribute(final String optionValues[]) {
363: Assert.notNull("optionValues", optionValues);
364: fakeSelectedValues_ = optionValues;
365: }
366:
367: /**
368: * Return an array of KeyValuePairs that are the values that will be sent
369: * back to the server whenever the current form is submitted.<p>
370: *
371: * THIS METHOD IS INTENDED FOR THE USE OF THE FRAMEWORK ONLY AND SHOULD NOT
372: * BE USED BY CONSUMERS OF HTMLUNIT. USE AT YOUR OWN RISK.
373: *
374: * @return See above
375: */
376: public KeyValuePair[] getSubmitKeyValuePairs() {
377: final String name = getNameAttribute();
378: final KeyValuePair[] pairs;
379:
380: if (ArrayUtils.isEmpty(fakeSelectedValues_)) {
381: final List selectedOptions = getSelectedOptions();
382: final int optionCount = selectedOptions.size();
383:
384: pairs = new KeyValuePair[optionCount];
385:
386: for (int i = 0; i < optionCount; i++) {
387: final HtmlOption option = (HtmlOption) selectedOptions
388: .get(i);
389: pairs[i] = new KeyValuePair(name, option
390: .getValueAttribute());
391: }
392: } else {
393: final List pairsList = new ArrayList();
394: for (int i = 0; i < fakeSelectedValues_.length; i++) {
395: if (fakeSelectedValues_[i].length() > 0) {
396: pairsList.add(new KeyValuePair(name,
397: fakeSelectedValues_[i]));
398: }
399: }
400: pairs = (KeyValuePair[]) pairsList
401: .toArray(new KeyValuePair[pairsList.size()]);
402: }
403: return pairs;
404: }
405:
406: /**
407: * Indicates if this select is submittable
408: * @return <code>false</code> if not
409: */
410: boolean isValidForSubmission() {
411: return getOptionSize() > 0
412: || (fakeSelectedValues_ != null && fakeSelectedValues_.length > 0);
413: }
414:
415: /**
416: * Return the value of this element to what it was at the time the page was loaded.
417: */
418: public void reset() {
419: final Iterator iterator = getOptions().iterator();
420: while (iterator.hasNext()) {
421: final HtmlOption option = (HtmlOption) iterator.next();
422: option.reset();
423: }
424: }
425:
426: /**
427: * {@inheritDoc}
428: * @see SubmittableElement#setDefaultValue(String)
429: */
430: public void setDefaultValue(final String defaultValue) {
431: setSelectedAttribute(defaultValue, true);
432: }
433:
434: /**
435: * {@inheritDoc}
436: * @see SubmittableElement#setDefaultValue(String)
437: */
438: public String getDefaultValue() {
439: final List options = getSelectedOptions();
440: if (options.size() > 0) {
441: return ((HtmlOption) options.get(0)).getValueAttribute();
442: } else {
443: return "";
444: }
445: }
446:
447: /**
448: * {@inheritDoc} This implementation is empty; only checkboxes and radio buttons
449: * really care what the default checked value is.
450: * @see SubmittableElement#setDefaultChecked(boolean)
451: * @see HtmlRadioButtonInput#setDefaultChecked(boolean)
452: * @see HtmlCheckBoxInput#setDefaultChecked(boolean)
453: */
454: public void setDefaultChecked(final boolean defaultChecked) {
455: // Empty.
456: }
457:
458: /**
459: * {@inheritDoc} This implementation returns <tt>false</tt>; only checkboxes and
460: * radio buttons really care what the default checked value is.
461: * @see SubmittableElement#isDefaultChecked()
462: * @see HtmlRadioButtonInput#isDefaultChecked()
463: * @see HtmlCheckBoxInput#isDefaultChecked()
464: */
465: public boolean isDefaultChecked() {
466: return false;
467: }
468:
469: /**
470: * Return true if this select is using "multiple select"
471: *
472: * @return See above
473: */
474: public boolean isMultipleSelectEnabled() {
475: return getAttributeValue("multiple") != ATTRIBUTE_NOT_DEFINED;
476: }
477:
478: /**
479: * Return the HtmlOption object that corresponds to the specified value
480: *
481: * @param value The value to search by
482: * @return See above
483: * @exception ElementNotFoundException If a particular xml element could not be found in the dom model
484: */
485: public HtmlOption getOptionByValue(final String value)
486: throws ElementNotFoundException {
487: Assert.notNull("value", value);
488:
489: return (HtmlOption) getOneHtmlElementByAttribute("option",
490: "value", value);
491: }
492:
493: /**
494: * Returns a text representation of this element that represents what would
495: * be visible to the user if this page was shown in a web browser. If the user
496: * can only select one option at a time, this method returns the selected option.
497: * If the user can select multiple options, this method returns all options.
498: *
499: * @return The element as text.
500: */
501: public String asText() {
502: final List options;
503: if (isMultipleSelectEnabled()) {
504: options = getOptions();
505: } else {
506: options = getSelectedOptions();
507: }
508:
509: final StringBuffer buffer = new StringBuffer();
510: for (final Iterator i = options.iterator(); i.hasNext();) {
511: final HtmlOption currentOption = (HtmlOption) i.next();
512: if (currentOption != null) {
513: buffer.append(currentOption.asText());
514: }
515: if (i.hasNext()) {
516: buffer.append("\n");
517: }
518: }
519:
520: return buffer.toString();
521: }
522:
523: /**
524: * Return the value of the attribute "name". Refer to the <a
525: * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for details on the use of this attribute.
526: *
527: * @return The value of the attribute "name" or an empty string if that attribute isn't defined.
528: */
529: public final String getNameAttribute() {
530: return getAttributeValue("name");
531: }
532:
533: /**
534: * Return the value of the attribute "size". Refer to the <a
535: * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
536: * details on the use of this attribute.
537: *
538: * @return The value of the attribute "size" or an empty string if that attribute isn't defined.
539: */
540: public final String getSizeAttribute() {
541: return getAttributeValue("size");
542: }
543:
544: /**
545: * Return the value of the attribute "multiple". Refer to the <a
546: * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for details on the use of this attribute.
547: *
548: * @return The value of the attribute "multiple" or an empty string if that attribute isn't defined.
549: */
550: public final String getMultipleAttribute() {
551: return getAttributeValue("multiple");
552: }
553:
554: /**
555: * Return the value of the attribute "disabled". Refer to the <a
556: * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for details on the use of this attribute.
557: *
558: * @return The value of the attribute "disabled" or an empty string if that attribute isn't defined.
559: */
560: public final String getDisabledAttribute() {
561: return getAttributeValue("disabled");
562: }
563:
564: /**
565: * Return true if the disabled attribute is set for this element.
566: *
567: * @return Return true if this element is disabled.
568: */
569: public final boolean isDisabled() {
570: return isAttributeDefined("disabled");
571: }
572:
573: /**
574: * Return the value of the attribute "tabindex". Refer to the <a
575: * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for details on the use of this attribute.
576: *
577: * @return The value of the attribute "tabindex" or an empty string if that attribute isn't defined.
578: */
579: public final String getTabIndexAttribute() {
580: return getAttributeValue("tabindex");
581: }
582:
583: /**
584: * Return the value of the attribute "onfocus". Refer to the <a
585: * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for details on the use of this attribute.
586: *
587: * @return The value of the attribute "onfocus" or an empty string if that attribute isn't defined.
588: */
589: public final String getOnFocusAttribute() {
590: return getAttributeValue("onfocus");
591: }
592:
593: /**
594: * Return the value of the attribute "onblur". Refer to the <a
595: * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for details on the use of this attribute.
596: *
597: * @return The value of the attribute "onblur" or an empty string if that attribute isn't defined.
598: */
599: public final String getOnBlurAttribute() {
600: return getAttributeValue("onblur");
601: }
602:
603: /**
604: * Return the value of the attribute "onchange". Refer to the <a
605: * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for details on the use of this attribute.
606: *
607: * @return The value of the attribute "onchange" or an empty string if that attribute isn't defined.
608: */
609: public final String getOnChangeAttribute() {
610: return getAttributeValue("onchange");
611: }
612: }
|