View Javadoc
1   /*
2    * Copyright (c) 2002-2017 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    * http://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 com.gargoylesoftware.htmlunit.javascript.host.html;
16  
17  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLCOLLECTION_ITEM_SUPPORTS_DOUBLE_INDEX_ALSO;
18  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLCOLLECTION_ITEM_SUPPORTS_ID_SEARCH_ALSO;
19  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLCOLLECTION_NAMED_ITEM_ID_FIRST;
20  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLCOLLECTION_SUPPORTS_PARANTHESES;
21  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.CHROME;
22  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.EDGE;
23  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.FF;
24  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.IE;
25  
26  import java.util.ArrayList;
27  import java.util.Collections;
28  import java.util.List;
29  
30  import com.gargoylesoftware.htmlunit.BrowserVersion;
31  import com.gargoylesoftware.htmlunit.html.DomElement;
32  import com.gargoylesoftware.htmlunit.html.DomNode;
33  import com.gargoylesoftware.htmlunit.html.HtmlForm;
34  import com.gargoylesoftware.htmlunit.html.HtmlInput;
35  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClass;
36  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxConstructor;
37  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction;
38  import com.gargoylesoftware.htmlunit.javascript.host.dom.AbstractList;
39  
40  import net.sourceforge.htmlunit.corejs.javascript.Context;
41  import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
42  
43  /**
44   * An array of elements. Used for the element arrays returned by <tt>document.all</tt>,
45   * <tt>document.all.tags('x')</tt>, <tt>document.forms</tt>, <tt>window.frames</tt>, etc.
46   * Note that this class must not be used for collections that can be modified, for example
47   * <tt>map.areas</tt> and <tt>select.options</tt>.
48   * <br>
49   * This class (like all classes in this package) is specific for the JavaScript engine.
50   * Users of HtmlUnit shouldn't use it directly.
51   *
52   * @author Daniel Gredler
53   * @author Marc Guillemot
54   * @author Chris Erskine
55   * @author Ahmed Ashour
56   * @author Frank Danek
57   */
58  @JsxClass
59  public class HTMLCollection extends AbstractList {
60  
61      /**
62       * IE provides a way of enumerating through some element collections; this counter supports that functionality.
63       */
64      private int currentIndex_;
65  
66      /**
67       * Creates an instance.
68       */
69      @JsxConstructor({CHROME, FF, EDGE})
70      public HTMLCollection() {
71      }
72  
73      /**
74       * Creates an instance.
75       * @param domNode parent scope
76       * @param attributeChangeSensitive indicates if the content of the collection may change when an attribute
77       * of a descendant node of parentScope changes (attribute added, modified or removed)
78       */
79      public HTMLCollection(final DomNode domNode, final boolean attributeChangeSensitive) {
80          super(domNode, attributeChangeSensitive);
81      }
82  
83      /**
84       * Constructs an instance with an initial cache value.
85       * @param domNode the parent scope, on which we listen for changes
86       * @param initialElements the initial content for the cache
87       */
88      HTMLCollection(final DomNode domNode, final List<DomNode> initialElements) {
89          super(domNode, initialElements);
90      }
91  
92      /**
93       * Gets an empty collection.
94       * @param domNode the DOM node
95       * @return an empty collection
96       */
97      public static HTMLCollection emptyCollection(final DomNode domNode) {
98          final List<DomNode> list = Collections.emptyList();
99          return new HTMLCollection(domNode, false) {
100             @Override
101             public List<DomNode> getElements() {
102                 return list;
103             }
104         };
105     }
106 
107     /**
108      * {@inheritDoc}
109      */
110     @Override
111     protected AbstractList create(final DomNode parentScope, final List<DomNode> initialElements) {
112         return new HTMLCollection(parentScope, initialElements);
113     }
114 
115     /**
116      * {@inheritDoc}
117      */
118     @Override
119     public Object call(final Context cx, final Scriptable scope, final Scriptable thisObj, final Object[] args) {
120         if (supportsParentheses()) {
121             return super.call(cx, scope, thisObj, args);
122         }
123 
124         throw Context.reportRuntimeError("TypeError - HTMLCollection does nont support function like access");
125     }
126 
127     /**
128      * Is parentheses supported.
129      *
130      * @return true or false
131      */
132     protected boolean supportsParentheses() {
133         return getBrowserVersion().hasFeature(HTMLCOLLECTION_SUPPORTS_PARANTHESES);
134     }
135 
136     /**
137      * {@inheritDoc}
138      */
139     @Override
140     protected Object getWithPreemptionByName(final String name, final List<DomNode> elements) {
141         final List<DomNode> matchingElements = new ArrayList<>();
142         final boolean searchName = isGetWithPreemptionSearchName();
143         for (final DomNode next : elements) {
144             if (next instanceof DomElement
145                     && (searchName || next instanceof HtmlInput || next instanceof HtmlForm)) {
146                 final String nodeName = ((DomElement) next).getAttribute("name");
147                 if (name.equals(nodeName)) {
148                     matchingElements.add(next);
149                 }
150             }
151         }
152 
153         if (matchingElements.isEmpty()) {
154             if (getBrowserVersion().hasFeature(HTMLCOLLECTION_ITEM_SUPPORTS_DOUBLE_INDEX_ALSO)) {
155                 final Double doubleValue = Context.toNumber(name);
156                 if (!doubleValue.isNaN()) {
157                     final Object object = get(doubleValue.intValue(), this);
158                     if (object != NOT_FOUND) {
159                         return object;
160                     }
161                 }
162             }
163             return NOT_FOUND;
164         }
165         else if (matchingElements.size() == 1) {
166             return getScriptableForElement(matchingElements.get(0));
167         }
168 
169         // many elements => build a sub collection
170         final DomNode domNode = getDomNodeOrNull();
171         final HTMLCollection collection = new HTMLCollection(domNode, matchingElements);
172         collection.setAvoidObjectDetection(true);
173         return collection;
174     }
175 
176     /**
177      * Returns whether {@link #getWithPreemption(String)} should search by name or not.
178      * @return whether {@link #getWithPreemption(String)} should search by name or not
179      */
180     protected boolean isGetWithPreemptionSearchName() {
181         return true;
182     }
183 
184     /**
185      * Returns the item or items corresponding to the specified index or key.
186      * @param index the index or key corresponding to the element or elements to return
187      * @return the element or elements corresponding to the specified index or key
188      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536460.aspx">MSDN doc</a>
189      */
190     @Override
191     public Object item(final Object index) {
192         if (index instanceof String && getBrowserVersion().hasFeature(HTMLCOLLECTION_ITEM_SUPPORTS_ID_SEARCH_ALSO)) {
193             final String name = (String) index;
194             final Object result = namedItem(name);
195             return result;
196         }
197 
198         int idx = 0;
199         final Double doubleValue = Context.toNumber(index);
200         if (!doubleValue.isNaN()) {
201             idx = doubleValue.intValue();
202         }
203 
204         final Object object = get(idx, this);
205         if (object == NOT_FOUND) {
206             return null;
207         }
208         return object;
209     }
210 
211     /**
212      * Retrieves the item or items corresponding to the specified name (checks ids, and if
213      * that does not work, then names).
214      * @param name the name or id the element or elements to return
215      * @return the element or elements corresponding to the specified name or id
216      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536634.aspx">MSDN doc</a>
217      */
218     @JsxFunction
219     public Object namedItem(final String name) {
220         final List<DomNode> elements = getElements();
221         final BrowserVersion browserVersion = getBrowserVersion();
222         if (browserVersion.hasFeature(HTMLCOLLECTION_NAMED_ITEM_ID_FIRST)) {
223             for (final Object next : elements) {
224                 if (next instanceof DomElement) {
225                     final DomElement elem = (DomElement) next;
226                     final String id = elem.getId();
227                     if (name.equals(id)) {
228                         return getScriptableForElement(elem);
229                     }
230                 }
231             }
232         }
233         for (final Object next : elements) {
234             if (next instanceof DomElement) {
235                 final DomElement elem = (DomElement) next;
236                 final String nodeName = elem.getAttribute("name");
237                 if (name.equals(nodeName)) {
238                     return getScriptableForElement(elem);
239                 }
240 
241                 final String id = elem.getId();
242                 if (name.equals(id)) {
243                     return getScriptableForElement(elem);
244                 }
245             }
246         }
247         return null;
248     }
249 
250     /**
251      * Returns the next node in the collection (supporting iteration in IE only).
252      * @return the next node in the collection
253      */
254     @JsxFunction(IE)
255     public Object nextNode() {
256         final Object nextNode;
257         final List<DomNode> elements = getElements();
258         if (currentIndex_ >= 0 && currentIndex_ < elements.size()) {
259             nextNode = elements.get(currentIndex_);
260         }
261         else {
262             nextNode = null;
263         }
264         currentIndex_++;
265         return nextNode;
266     }
267 
268     /**
269      * Resets the node iterator accessed via {@link #nextNode()}.
270      */
271     @JsxFunction(IE)
272     public void reset() {
273         currentIndex_ = 0;
274     }
275 
276     /**
277      * Returns all the elements in this element array that have the specified tag name.
278      * This method returns an empty element array if there are no elements with the
279      * specified tag name.
280      * @param tagName the name of the tag of the elements to return
281      * @return all the elements in this element array that have the specified tag name
282      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536776.aspx">MSDN doc</a>
283      */
284     @JsxFunction(IE)
285     public Object tags(final String tagName) {
286         final HTMLCollection collection = new HTMLSubCollection(this) {
287             @Override
288             protected boolean isMatching(final DomNode node) {
289                 return tagName.equalsIgnoreCase(node.getLocalName());
290             }
291         };
292         return collection;
293     }
294 }
295 
296 class HTMLSubCollection extends HTMLCollection {
297     private final HTMLCollection mainCollection_;
298 
299     HTMLSubCollection(final HTMLCollection mainCollection) {
300         super(mainCollection.getDomNodeOrDie(), false);
301         mainCollection_ = mainCollection;
302     }
303 
304     @Override
305     protected List<DomNode> computeElements() {
306         final List<DomNode> list = new ArrayList<>();
307         for (final DomNode o : mainCollection_.getElements()) {
308             if (isMatching(o)) {
309                 list.add(o);
310             }
311         }
312         return list;
313     }
314 }