View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.html;
16  
17  import java.util.ArrayList;
18  import java.util.Collections;
19  import java.util.Iterator;
20  import java.util.List;
21  import java.util.Map;
22  
23  import org.htmlunit.ElementNotFoundException;
24  import org.htmlunit.Page;
25  import org.htmlunit.SgmlPage;
26  import org.htmlunit.WebAssert;
27  import org.htmlunit.javascript.host.event.Event;
28  import org.htmlunit.javascript.host.event.MouseEvent;
29  import org.htmlunit.util.NameValuePair;
30  import org.htmlunit.util.StringUtils;
31  import org.w3c.dom.Node;
32  
33  /**
34   * Wrapper for the HTML element "select".
35   *
36   * @author Mike Bowler
37   * @author Mike J. Bresnahan
38   * @author David K. Taylor
39   * @author Christian Sell
40   * @author David D. Kilzer
41   * @author Marc Guillemot
42   * @author Daniel Gredler
43   * @author Ahmed Ashour
44   * @author Ronald Brill
45   * @author Frank Danek
46   * @author Lai Quang Duong
47   */
48  public class HtmlSelect extends HtmlElement implements DisabledElement, SubmittableElement,
49                  LabelableElement, ValidatableHtmlElement {
50  
51      /** The HTML tag represented by this element. */
52      public static final String TAG_NAME = "select";
53  
54      /** The index of the HtmlOption which was last selected. */
55      private int lastSelectedIndex_ = -1;
56      private String customValidity_;
57  
58      /**
59       * Creates an instance.
60       *
61       * @param qualifiedName the qualified name of the element type to instantiate
62       * @param page the page that contains this element
63       * @param attributes the initial attributes
64       */
65      HtmlSelect(final String qualifiedName, final SgmlPage page,
66              final Map<String, DomAttr> attributes) {
67          super(qualifiedName, page, attributes);
68      }
69  
70      /**
71       * If we were given an invalid <code>size</code> attribute, normalize it.
72       * Then set a default selected option if none was specified and the size is 1 or less
73       * and this isn't a multiple selection input.
74       * @param postponed unused in this implementation; kept to satisfy the overridden signature,
75       *     see {@link org.htmlunit.javascript.PostponedAction}
76       */
77      @Override
78      public void onAllChildrenAddedToPage(final boolean postponed) {
79          // Fix the size if necessary.
80          int size;
81          try {
82              size = Integer.parseInt(getSizeAttribute());
83              if (size < 0) {
84                  size = 0;
85              }
86          }
87          catch (final NumberFormatException e) {
88              size = 0;
89          }
90  
91          // Set a default selected option if necessary.
92          if (getSelectedOptions().isEmpty() && size <= 1 && !isMultipleSelectEnabled()) {
93              final List<HtmlOption> options = getOptions();
94              if (!options.isEmpty()) {
95                  final HtmlOption first = options.get(0);
96                  first.setSelectedInternal(true);
97              }
98          }
99      }
100 
101     /**
102      * {@inheritDoc}
103      */
104     @Override
105     public boolean handles(final Event event) {
106         if (event instanceof MouseEvent) {
107             return true;
108         }
109 
110         return super.handles(event);
111     }
112 
113     /**
114      * <p>Returns all the currently selected options. The following special
115      * conditions can occur if the element is in single select mode:</p>
116      * <ul>
117      *   <li>if multiple options are erroneously selected, the last one is returned</li>
118      *   <li>if no options are selected, an empty list is returned</li>
119      * </ul>
120      *
121      * @return the currently selected options
122      */
123     public List<HtmlOption> getSelectedOptions() {
124         final List<HtmlOption> result;
125         if (isMultipleSelectEnabled()) {
126             // Multiple selections possible.
127             result = new ArrayList<>();
128             for (final HtmlElement element : getHtmlElementDescendants()) {
129                 if (element instanceof HtmlOption option && option.isSelected()) {
130                     result.add(option);
131                 }
132             }
133         }
134         else {
135             // Only a single selection is possible.
136             result = new ArrayList<>(1);
137             HtmlOption lastSelected = null;
138             for (final HtmlElement element : getHtmlElementDescendants()) {
139                 if (element instanceof HtmlOption option) {
140                     if (option.isSelected()) {
141                         lastSelected = option;
142                     }
143                 }
144             }
145             if (lastSelected != null) {
146                 result.add(lastSelected);
147             }
148         }
149         return Collections.unmodifiableList(result);
150     }
151 
152     /**
153      * Returns all the options in this select element.
154      * @return all the options in this select element
155      */
156     public List<HtmlOption> getOptions() {
157         return Collections.unmodifiableList(getStaticElementsByTagName("option"));
158     }
159 
160     /**
161      * Returns the indexed option.
162      *
163      * @param index the index
164      * @return the option specified by the index
165      */
166     public HtmlOption getOption(final int index) {
167         return this.<HtmlOption>getStaticElementsByTagName("option").get(index);
168     }
169 
170     /**
171      * Returns the number of options.
172      * @return the number of options
173      */
174     public int getOptionSize() {
175         return getStaticElementsByTagName("option").size();
176     }
177 
178     /**
179      * Remove options by reducing the "length" property. This has no
180      * effect if the length is set to the same or greater.
181      * @param newLength the new length property value
182      */
183     public void setOptionSize(final int newLength) {
184         final List<HtmlElement> elementList = getStaticElementsByTagName("option");
185 
186         for (int i = elementList.size() - 1; i >= newLength; i--) {
187             elementList.get(i).remove();
188         }
189     }
190 
191     /**
192      * Remove an option at the given index. This is a no-op (no exception thrown)
193      * if no option exists at the given index.
194      * @param index the index of the option to remove
195      */
196     public void removeOption(final int index) {
197         final Iterator<HtmlElement> iterator = getHtmlElementDescendants().iterator();
198         int i = 0;
199         while (iterator.hasNext()) {
200             final DomElement element = iterator.next();
201             if (element instanceof HtmlOption) {
202                 if (i == index) {
203                     element.remove();
204                     ensureSelectedIndex();
205                     return;
206                 }
207                 i++;
208             }
209         }
210     }
211 
212     /**
213      * Replace an option at the given index with a new option. This is a no-op
214      * (no exception thrown) if no option exists at the given index.
215      * @param index the index of the option to remove
216      * @param newOption the new option to replace to indexed option
217      */
218     public void replaceOption(final int index, final HtmlOption newOption) {
219         final Iterator<HtmlElement> iterator = getHtmlElementDescendants().iterator();
220         int i = 0;
221         while (iterator.hasNext()) {
222             final DomElement element = iterator.next();
223             if (element instanceof HtmlOption) {
224                 if (i == index) {
225                     element.replace(newOption);
226 
227                     if (newOption.isSelected() && !isMultipleSelectEnabled()) {
228                         setOnlySelected(newOption, true);
229                     }
230                     ensureSelectedIndex();
231 
232                     return;
233                 }
234                 i++;
235             }
236         }
237     }
238 
239     /**
240      * Add a new option at the end.
241      * @param newOption the new option to add
242      */
243     public void appendOption(final HtmlOption newOption) {
244         appendChild(newOption);
245 
246         ensureSelectedIndex();
247     }
248 
249     /**
250      * {@inheritDoc}
251      * If the appended node is a selected {@link HtmlOption}, this select's
252      * selection state (and any related deselection of sibling options) is
253      * updated accordingly.
254      */
255     @Override
256     public DomNode appendChild(final Node node) {
257         final DomNode response = super.appendChild(node);
258         if (node instanceof HtmlOption option) {
259             if (option.isSelected()) {
260                 doSelectOption(option, true, false, false, false);
261             }
262         }
263         return response;
264     }
265 
266     /**
267      * Sets the "selected" state of the specified option. If this "select" element
268      * is single-select, then calling this method will deselect all other options.
269      * <p>
270      * Only options that are actually in the document may be selected.
271      * </p>
272      *
273      * @param <P> the page type
274      * @param optionValue the value of the option that is to change
275      * @param isSelected true if the option is to become selected
276      * @return the page contained in the current window as returned
277      *         by {@link org.htmlunit.WebClient#getCurrentWindow()}
278      */
279     public <P extends Page> P setSelectedAttribute(final String optionValue, final boolean isSelected) {
280         return setSelectedAttribute(optionValue, isSelected, true);
281     }
282 
283     /**
284      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
285      *
286      * Sets the "selected" state of the specified option. If this "select" element
287      * is single-select, then calling this method will deselect all other options.
288      * <p>
289      * Only options that are actually in the document may be selected.
290      * </p>
291      *
292      * @param <P> the page type
293      * @param optionValue the value of the option that is to change
294      * @param isSelected true if the option is to become selected
295      * @param invokeOnFocus whether to set focus or not.
296      * @return the page contained in the current window as returned
297      *         by {@link org.htmlunit.WebClient#getCurrentWindow()}
298      */
299     @SuppressWarnings("unchecked")
300     public <P extends Page> P setSelectedAttribute(final String optionValue,
301             final boolean isSelected, final boolean invokeOnFocus) {
302         try {
303             final HtmlOption selected = getOptionByValue(optionValue);
304             return setSelectedAttribute(selected, isSelected, invokeOnFocus, true, false, true);
305         }
306         catch (final ElementNotFoundException e) {
307             for (final HtmlOption o : getSelectedOptions()) {
308                 o.setSelected(false);
309             }
310             return (P) getPage();
311         }
312     }
313 
314     /**
315      * Sets the "selected" state of the specified option. If this "select" element
316      * is single-select, then calling this method will deselect all other options.
317      * <p>
318      * Only options that are actually in the document may be selected.
319      * </p>
320      *
321      * @param <P> the page type
322      * @param selectedOption the option that is to change
323      * @param isSelected true if the option is to become selected
324      * @return the page contained in the current window as returned
325      *         by {@link org.htmlunit.WebClient#getCurrentWindow()}
326      */
327     public <P extends Page> P setSelectedAttribute(final HtmlOption selectedOption, final boolean isSelected) {
328         return setSelectedAttribute(selectedOption, isSelected, true, true, false, true);
329     }
330 
331     /**
332      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
333      *
334      * Sets the "selected" state of the specified option. If this "select" element
335      * is single-select, then calling this method will deselect all other options.
336      * <p>
337      * Only options that are actually in the document may be selected.
338      * </p>
339      *
340      * @param <P> the page type
341      * @param selectedOption the option that is to change
342      * @param isSelected true if the option is to become selected
343      * @param invokeOnFocus whether to set focus or not.
344      * @param shiftKey {@code true} if SHIFT is pressed
345      * @param ctrlKey {@code true} if CTRL is pressed
346      * @param isClick is mouse clicked
347      * @return the page contained in the current window as returned
348      *         by {@link org.htmlunit.WebClient#getCurrentWindow()}
349      */
350     @SuppressWarnings("unchecked")
351     public <P extends Page> P setSelectedAttribute(final HtmlOption selectedOption, final boolean isSelected,
352         final boolean invokeOnFocus, final boolean shiftKey, final boolean ctrlKey, final boolean isClick) {
353         if (isSelected && invokeOnFocus) {
354             ((HtmlPage) getPage()).setFocusedElement(this);
355         }
356 
357         final boolean changeSelectedState = selectedOption.isSelected() != isSelected;
358 
359         if (changeSelectedState) {
360             doSelectOption(selectedOption, isSelected, shiftKey, ctrlKey, isClick);
361             HtmlInput.executeOnChangeHandlerIfAppropriate(this);
362         }
363 
364         return (P) getPage().getWebClient().getCurrentWindow().getEnclosedPage();
365     }
366 
367     private void doSelectOption(final HtmlOption selectedOption,
368             final boolean isSelected, final boolean shiftKey, final boolean ctrlKey, final boolean isClick) {
369         // caution the HtmlOption may have been created from js and therefore the select now need
370         // to "know" that it is selected
371         if (isMultipleSelectEnabled()) {
372             selectedOption.setSelectedInternal(isSelected);
373             if (isClick && !ctrlKey) {
374                 if (!shiftKey) {
375                     setOnlySelected(selectedOption, isSelected);
376                     lastSelectedIndex_ = getOptions().indexOf(selectedOption);
377                 }
378                 else if (isSelected && lastSelectedIndex_ != -1) {
379                     final List<HtmlOption> options = getOptions();
380                     final int newIndex = options.indexOf(selectedOption);
381                     for (int i = 0; i < options.size(); i++) {
382                         options.get(i).setSelectedInternal(isBetween(i, lastSelectedIndex_, newIndex));
383                     }
384                 }
385             }
386         }
387         else {
388             setOnlySelected(selectedOption, isSelected);
389         }
390     }
391 
392     /**
393      * Sets the given {@link HtmlOption} as the only selected one.
394      * @param selectedOption the selected {@link HtmlOption}
395      * @param isSelected whether selected or not
396      */
397     void setOnlySelected(final HtmlOption selectedOption, final boolean isSelected) {
398         for (final HtmlOption option : getOptions()) {
399             option.setSelectedInternal(option == selectedOption && isSelected);
400         }
401     }
402 
403     private static boolean isBetween(final int number, final int min, final int max) {
404         return max > min ? number >= min && number <= max : number >= max && number <= min;
405     }
406 
407     /**
408      * {@inheritDoc}
409      */
410     @Override
411     public NameValuePair[] getSubmitNameValuePairs() {
412         final String name = getNameAttribute();
413 
414         final List<HtmlOption> selectedOptions = getSelectedOptions();
415 
416         final NameValuePair[] pairs = new NameValuePair[selectedOptions.size()];
417 
418         int i = 0;
419         for (final HtmlOption option : selectedOptions) {
420             pairs[i++] = new NameValuePair(name, option.getValueAttribute());
421         }
422         return pairs;
423     }
424 
425     /**
426      * Indicates if this select is submittable.
427      * @return {@code false} if not
428      */
429     boolean isValidForSubmission() {
430         return getOptionSize() > 0;
431     }
432 
433     /**
434      * Returns the value of this element to what it was at the time the page was loaded.
435      */
436     @Override
437     public void reset() {
438         for (final HtmlOption option : getOptions()) {
439             option.reset();
440         }
441         onAllChildrenAddedToPage(false);
442     }
443 
444     /**
445      * {@inheritDoc}
446      * @see SubmittableElement#setDefaultValue(String)
447      */
448     @Override
449     public void setDefaultValue(final String defaultValue) {
450         setSelectedAttribute(defaultValue, true);
451     }
452 
453     /**
454      * {@inheritDoc}
455      * @see SubmittableElement#setDefaultValue(String)
456      */
457     @Override
458     public String getDefaultValue() {
459         final List<HtmlOption> options = getSelectedOptions();
460         if (options.isEmpty()) {
461             return "";
462         }
463         return options.get(0).getValueAttribute();
464     }
465 
466     /**
467      * {@inheritDoc}
468      * This implementation is empty; only checkboxes and radio buttons
469      * really care what the default checked value is.
470      * @see SubmittableElement#setDefaultChecked(boolean)
471      * @see HtmlRadioButtonInput#setDefaultChecked(boolean)
472      * @see HtmlCheckBoxInput#setDefaultChecked(boolean)
473      */
474     @Override
475     public void setDefaultChecked(final boolean defaultChecked) {
476         // Empty.
477     }
478 
479     /**
480      * {@inheritDoc}
481      * This implementation returns {@code false}; only checkboxes and
482      * radio buttons really care what the default checked value is.
483      * @see SubmittableElement#isDefaultChecked()
484      * @see HtmlRadioButtonInput#isDefaultChecked()
485      * @see HtmlCheckBoxInput#isDefaultChecked()
486      */
487     @Override
488     public boolean isDefaultChecked() {
489         return false;
490     }
491 
492     /**
493      * Returns {@code true} if this select is using "multiple select".
494      * @return {@code true} if this select is using "multiple select"
495      */
496     public boolean isMultipleSelectEnabled() {
497         return getAttributeDirect("multiple") != ATTRIBUTE_NOT_DEFINED;
498     }
499 
500     /**
501      * Returns the {@link HtmlOption} object that corresponds to the specified value.
502      *
503      * @param value the value to search by
504      * @return the {@link HtmlOption} object that corresponds to the specified value
505      * @throws ElementNotFoundException If a particular element could not be found in the DOM model
506      */
507     public HtmlOption getOptionByValue(final String value) throws ElementNotFoundException {
508         WebAssert.notNull(VALUE_ATTRIBUTE, value);
509         for (final HtmlOption option : getOptions()) {
510             if (option.getValueAttribute().equals(value)) {
511                 return option;
512             }
513         }
514         throw new ElementNotFoundException("option", VALUE_ATTRIBUTE, value);
515     }
516 
517     /**
518      * Returns the {@link HtmlOption} object that has the specified text.
519      *
520      * @param text the text to search by
521      * @return the {@link HtmlOption} object that has the specified text
522      * @throws ElementNotFoundException If a particular element could not be found in the DOM model
523      */
524     public HtmlOption getOptionByText(final String text) throws ElementNotFoundException {
525         WebAssert.notNull("text", text);
526         for (final HtmlOption option : getOptions()) {
527             if (option.getText().equals(text)) {
528                 return option;
529             }
530         }
531         throw new ElementNotFoundException("option", "text", text);
532     }
533 
534     /**
535      * Returns the value of the attribute {@code name}. Refer to the <a
536      * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
537      *
538      * @return the value of the attribute {@code name} or an empty string if that attribute isn't defined
539      */
540     public final String getNameAttribute() {
541         return getAttributeDirect(NAME_ATTRIBUTE);
542     }
543 
544     /**
545      * Returns the value of the attribute {@code size}. Refer to the <a
546      * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for
547      * details on the use of this attribute.
548      *
549      * @return the value of the attribute {@code size} or an empty string if that attribute isn't defined
550      */
551     public final String getSizeAttribute() {
552         return getAttributeDirect("size");
553     }
554 
555     /**
556      * Returns the size or 0 if not defined or not convertable to int.
557      * NOTE: this currently diverges from the HTML specification, which defines
558      * the default {@code size} IDL value as 1 for a single select and 4 for a
559      * multiple select.
560      *
561      * @return the size or 0 if not defined or not convertable to int
562      */
563     public final int getSize() {
564         final String sizeAttribute = getSizeAttribute();
565         if (ATTRIBUTE_NOT_DEFINED != sizeAttribute && ATTRIBUTE_VALUE_EMPTY != sizeAttribute) {
566             try {
567                 final int size = Integer.parseInt(getSizeAttribute());
568                 if (size < 0) {
569                     return 0;
570                 }
571                 return size;
572             }
573             catch (final NumberFormatException ignored) {
574                 // silently ignore
575             }
576         }
577         return 0;
578     }
579 
580     /**
581      * Returns the value of the attribute {@code multiple}. Refer to the <a
582      * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
583      *
584      * @return the value of the attribute {@code multiple} or an empty string if that attribute isn't defined
585      */
586     public final String getMultipleAttribute() {
587         return getAttributeDirect("multiple");
588     }
589 
590     /**
591      * {@inheritDoc}
592      */
593     @Override
594     public final String getDisabledAttribute() {
595         return getAttributeDirect(ATTRIBUTE_DISABLED);
596     }
597 
598     /**
599      * Returns the value of the attribute {@code tabindex}. Refer to the <a
600      * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
601      *
602      * @return the value of the attribute {@code tabindex} or an empty string if that attribute isn't defined
603      */
604     public final String getTabIndexAttribute() {
605         return getAttributeDirect("tabindex");
606     }
607 
608     /**
609      * Returns the value of the attribute {@code onfocus}. Refer to the <a
610      * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
611      *
612      * @return the value of the attribute {@code onfocus} or an empty string if that attribute isn't defined
613      */
614     public final String getOnFocusAttribute() {
615         return getAttributeDirect("onfocus");
616     }
617 
618     /**
619      * Returns the value of the attribute {@code onblur}. Refer to the <a
620      * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
621      *
622      * @return the value of the attribute {@code onblur} or an empty string if that attribute isn't defined
623      */
624     public final String getOnBlurAttribute() {
625         return getAttributeDirect("onblur");
626     }
627 
628     /**
629      * Returns the value of the attribute {@code onchange}. Refer to the <a
630      * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
631      *
632      * @return the value of the attribute {@code onchange} or an empty string if that attribute isn't defined
633      */
634     public final String getOnChangeAttribute() {
635         return getAttributeDirect("onchange");
636     }
637 
638     /**
639      * {@inheritDoc}
640      */
641     @Override
642     public DisplayStyle getDefaultStyleDisplay() {
643         return DisplayStyle.INLINE_BLOCK;
644     }
645 
646     /**
647      * Returns the value of the {@code selectedIndex} property.
648      * @return the selectedIndex property
649      */
650     public int getSelectedIndex() {
651         final List<HtmlOption> selectedOptions = getSelectedOptions();
652         if (selectedOptions.isEmpty()) {
653             return -1;
654         }
655         final List<HtmlOption> allOptions = getOptions();
656         return allOptions.indexOf(selectedOptions.get(0));
657     }
658 
659     /**
660      * Sets the value of the {@code selectedIndex} property. Any previously selected
661      * options are always deselected first; if {@code index} is negative or is out of
662      * range for the current number of options, the result is that nothing is selected.
663      * @param index the new value
664      */
665     public void setSelectedIndex(final int index) {
666         for (final HtmlOption itemToUnSelect : getSelectedOptions()) {
667             setSelectedAttribute(itemToUnSelect, false);
668         }
669         if (index < 0) {
670             return;
671         }
672 
673         final List<HtmlOption> allOptions = getOptions();
674 
675         if (index < allOptions.size()) {
676             final HtmlOption itemToSelect = allOptions.get(index);
677             setSelectedAttribute(itemToSelect, true, false, true, false, true);
678         }
679     }
680 
681     /**
682      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
683      *
684      * Resets the selectedIndex if needed.
685      */
686     public void ensureSelectedIndex() {
687         if (getOptionSize() == 0) {
688             setSelectedIndex(-1);
689         }
690         else if (getSelectedIndex() == -1 && !isMultipleSelectEnabled()) {
691             setSelectedIndex(0);
692         }
693     }
694 
695     /**
696      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
697      *
698      * @param option the option to search for
699      * @return the index of the provided option or zero if not found. CAUTION: this is
700      *     ambiguous with an option that is genuinely found at index zero; callers cannot
701      *     distinguish "not found" from "found as the first option" from the return value alone
702      */
703     public int indexOf(final HtmlOption option) {
704         if (option == null) {
705             return 0;
706         }
707 
708         int index = 0;
709         for (final HtmlElement element : getHtmlElementDescendants()) {
710             if (element instanceof HtmlOption) {
711                 if (option == element) {
712                     return index;
713                 }
714                 index++;
715             }
716         }
717         return 0;
718     }
719 
720     /**
721      * {@inheritDoc}
722      */
723     @Override
724     protected boolean isRequiredSupported() {
725         return true;
726     }
727 
728     /**
729      * {@inheritDoc}
730      */
731     @Override
732     public boolean willValidate() {
733         return !isDisabled();
734     }
735 
736     /**
737      * {@inheritDoc}
738      */
739     @Override
740     public String getCustomValidity() {
741         return customValidity_;
742     }
743 
744     /**
745      * {@inheritDoc}
746      */
747     @Override
748     public void setCustomValidity(final String message) {
749         customValidity_ = message;
750     }
751 
752     /**
753      * {@inheritDoc}
754      */
755     @Override
756     public boolean isValid() {
757         return isValidValidityState();
758     }
759 
760     /**
761      * {@inheritDoc}
762      */
763     @Override
764     public boolean isCustomErrorValidityState() {
765         return !StringUtils.isEmptyOrNull(customValidity_);
766     }
767 
768     /**
769      * {@inheritDoc}
770      */
771     @Override
772     public boolean isValidValidityState() {
773         return !isCustomErrorValidityState()
774                 && !isValueMissingValidityState();
775     }
776 
777     /**
778      * {@inheritDoc}
779      */
780     @Override
781     public boolean isValueMissingValidityState() {
782         if (ATTRIBUTE_NOT_DEFINED == getAttributeDirect(ATTRIBUTE_REQUIRED)) {
783             return false;
784         }
785 
786         final List<HtmlOption> selected = getSelectedOptions();
787         if (selected.isEmpty()) {
788             return true;
789         }
790 
791         // per spec, this only applies to single-selection selects; a multi-select
792         // or size>1 select with at least one option selected is never "missing"
793         if (!isMultipleSelectEnabled() && getSize() <= 1) {
794             final List<HtmlOption> options = getOptions();
795             return !options.isEmpty()
796                     && selected.size() == 1
797                     && selected.get(0) == options.get(0)
798                     && selected.get(0).getValueAttribute().isEmpty();
799         }
800 
801         return false;
802     }
803 
804     /**
805      * {@inheritDoc}
806      */
807     @Override
808     public String getValueMissingMessage() {
809         return "Please select an item in the list.";
810     }
811 }