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.dom;
16  
17  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLCOLLECTION_NULL_IF_ITEM_NOT_FOUND;
18  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLCOLLECTION_NULL_IF_NOT_FOUND;
19  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_NODE_LIST_ENUMERATE_CHILDREN;
20  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_NODE_LIST_ENUMERATE_FUNCTIONS;
21  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.CHROME;
22  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.FF;
23  
24  import java.lang.ref.WeakReference;
25  import java.util.ArrayList;
26  import java.util.List;
27  
28  import com.gargoylesoftware.htmlunit.BrowserVersion;
29  import com.gargoylesoftware.htmlunit.html.DomChangeEvent;
30  import com.gargoylesoftware.htmlunit.html.DomChangeListener;
31  import com.gargoylesoftware.htmlunit.html.DomElement;
32  import com.gargoylesoftware.htmlunit.html.DomNode;
33  import com.gargoylesoftware.htmlunit.html.HtmlAttributeChangeEvent;
34  import com.gargoylesoftware.htmlunit.html.HtmlAttributeChangeListener;
35  import com.gargoylesoftware.htmlunit.html.HtmlElement;
36  import com.gargoylesoftware.htmlunit.html.HtmlPage;
37  import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
38  import com.gargoylesoftware.htmlunit.javascript.configuration.JavaScriptConfiguration;
39  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClass;
40  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxConstructor;
41  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction;
42  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxGetter;
43  
44  import net.sourceforge.htmlunit.corejs.javascript.Context;
45  import net.sourceforge.htmlunit.corejs.javascript.Function;
46  import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
47  import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject;
48  import net.sourceforge.htmlunit.corejs.javascript.Undefined;
49  
50  /**
51   * The parent class of {@link NodeList} and {@link com.gargoylesoftware.htmlunit.javascript.host.html.HTMLCollection}.
52   *
53   * @author Daniel Gredler
54   * @author Marc Guillemot
55   * @author Chris Erskine
56   * @author Ahmed Ashour
57   * @author Frank Danek
58   */
59  @JsxClass(isJSObject = false)
60  public class AbstractList extends SimpleScriptable implements Function {
61  
62      /**
63       * Cache effect of some changes.
64       */
65      protected enum EffectOnCache {
66          /** No effect, cache is still valid. */
67          NONE,
68          /** Cache is not valid anymore and should be reset. */
69          RESET
70      }
71  
72      private boolean avoidObjectDetection_;
73  
74      private boolean attributeChangeSensitive_;
75  
76      /**
77       * Cache collection elements when possible, so as to avoid expensive XPath expression evaluations.
78       */
79      private List<DomNode> cachedElements_;
80  
81      private boolean listenerRegistered_;
82  
83      /**
84       * Creates an instance.
85       */
86      @JsxConstructor({CHROME, FF})
87      public AbstractList() {
88      }
89  
90      /**
91       * Creates an instance.
92       *
93       * @param domeNode the {@link DomNode}
94       * @param attributeChangeSensitive indicates if the content of the collection may change when an attribute
95       * of a descendant node of parentScope changes (attribute added, modified or removed)
96       */
97      public AbstractList(final DomNode domeNode, final boolean attributeChangeSensitive) {
98          this(domeNode, attributeChangeSensitive, null);
99      }
100 
101     /**
102      * Creates an instance with an initial cache value.
103      *
104      * @param domNode the {@link DomNode}
105      * @param initialElements the initial content for the cache
106      */
107     protected AbstractList(final DomNode domNode, final List<DomNode> initialElements) {
108         this(domNode, true, new ArrayList<>(initialElements));
109     }
110 
111     /**
112      * Creates an instance.
113      *
114      * @param domNode the {@link DomNode}
115      * @param attributeChangeSensitive indicates if the content of the collection may change when an attribute
116      * of a descendant node of parentScope changes (attribute added, modified or removed)
117      * @param initialElements the initial content for the cache
118      */
119     private AbstractList(final DomNode domNode, final boolean attributeChangeSensitive,
120             final List<DomNode> initialElements) {
121         if (domNode != null) {
122             setDomNode(domNode, false);
123             final ScriptableObject parentScope = domNode.getScriptableObject();
124             if (parentScope != null) {
125                 setParentScope(parentScope);
126                 setPrototype(getPrototype(getClass()));
127             }
128         }
129         attributeChangeSensitive_ = attributeChangeSensitive;
130         cachedElements_ = initialElements;
131     }
132 
133     /**
134      * Only needed to make collections like <tt>document.all</tt> available but "invisible" when simulating Firefox.
135      * {@inheritDoc}
136      */
137     @Override
138     public boolean avoidObjectDetection() {
139         return avoidObjectDetection_;
140     }
141 
142     /**
143      * @param newValue the new value
144      */
145     public void setAvoidObjectDetection(final boolean newValue) {
146         avoidObjectDetection_ = newValue;
147     }
148 
149     /**
150      * {@inheritDoc}
151      */
152     @Override
153     public Object call(final Context cx, final Scriptable scope, final Scriptable thisObj, final Object[] args) {
154         if (args.length == 0) {
155             throw Context.reportRuntimeError("Zero arguments; need an index or a key.");
156         }
157         final Object object = getIt(args[0]);
158         if (object == NOT_FOUND) {
159             if (getBrowserVersion().hasFeature(HTMLCOLLECTION_NULL_IF_NOT_FOUND)) {
160                 return null;
161             }
162             return Undefined.instance;
163         }
164         return object;
165     }
166 
167     /**
168      * {@inheritDoc}
169      */
170     @Override
171     public final Scriptable construct(final Context cx, final Scriptable scope, final Object[] args) {
172         return null;
173     }
174 
175     /**
176      * Private helper that retrieves the item or items corresponding to the specified
177      * index or key.
178      * @param o the index or key corresponding to the element or elements to return
179      * @return the element or elements corresponding to the specified index or key
180      */
181     private Object getIt(final Object o) {
182         if (o instanceof Number) {
183             final Number n = (Number) o;
184             final int i = n.intValue();
185             return get(i, this);
186         }
187         final String key = String.valueOf(o);
188         return get(key, this);
189     }
190 
191     /**
192      * Returns the element at the specified index, or {@link #NOT_FOUND} if the index is invalid.
193      * {@inheritDoc}
194      */
195     @Override
196     public final Object get(final int index, final Scriptable start) {
197         final AbstractList array = (AbstractList) start;
198         final List<DomNode> elements = array.getElements();
199         if (index >= 0 && index < elements.size()) {
200             return getScriptableForElement(elements.get(index));
201         }
202         return NOT_FOUND;
203     }
204 
205     @Override
206     protected void setDomNode(final DomNode domNode, final boolean assignScriptObject) {
207         final DomNode oldDomNode = getDomNodeOrNull();
208 
209         super.setDomNode(domNode, assignScriptObject);
210 
211         if (oldDomNode != domNode) {
212             listenerRegistered_ = false;
213         }
214     }
215 
216     /**
217      * Gets the HTML elements from cache or retrieve them at first call.
218      * @return the list of {@link HtmlElement} contained in this collection
219      */
220     public List<DomNode> getElements() {
221         // a bit strange but we like to avoid sync
222         List<DomNode> cachedElements = cachedElements_;
223 
224         if (cachedElements == null) {
225             if (getParentScope() == null) {
226                 cachedElements = new ArrayList<>();
227             }
228             else {
229                 cachedElements = computeElements();
230             }
231             cachedElements_ = cachedElements;
232             if (!listenerRegistered_) {
233                 final DomHtmlAttributeChangeListenerImpl listener = new DomHtmlAttributeChangeListenerImpl(this);
234                 final DomNode domNode = getDomNodeOrNull();
235                 if (domNode != null) {
236                     domNode.addDomChangeListener(listener);
237                     if (attributeChangeSensitive_) {
238                         if (domNode instanceof HtmlElement) {
239                             ((HtmlElement) domNode).addHtmlAttributeChangeListener(listener);
240                         }
241                         else if (domNode instanceof HtmlPage) {
242                             ((HtmlPage) domNode).addHtmlAttributeChangeListener(listener);
243                         }
244                     }
245                     listenerRegistered_ = true;
246                 }
247             }
248         }
249 
250         // maybe the cache was cleared in between
251         // then this returns the old state and never null
252         return cachedElements;
253     }
254 
255     /**
256      * Returns the elements whose associated host objects are available through this collection.
257      * @return the elements whose associated host objects are available through this collection
258      */
259     protected List<DomNode> computeElements() {
260         final List<DomNode> response = new ArrayList<>();
261         final DomNode domNode = getDomNodeOrNull();
262         if (domNode == null) {
263             return response;
264         }
265         for (final DomNode node : getCandidates()) {
266             if (node instanceof DomElement && isMatching(node)) {
267                 response.add(node);
268             }
269         }
270         return response;
271     }
272 
273     /**
274      * Gets the DOM node that have to be examined to see if they are matching.
275      * Default implementation looks at all descendants of reference node.
276      * @return the nodes
277      */
278     protected Iterable<DomNode> getCandidates() {
279         final DomNode domNode = getDomNodeOrNull();
280         return domNode.getDescendants();
281     }
282 
283     /**
284      * Indicates if the node should belong to the collection.
285      * Belongs to the refactoring effort to improve HTMLCollection's performance.
286      * @param node the node to test. Will be a child node of the reference node.
287      * @return {@code false} here as subclasses for concrete collections should decide it.
288      */
289     protected boolean isMatching(final DomNode node) {
290         return false;
291     }
292 
293     /**
294      * Returns the element or elements that match the specified key. If it is the name
295      * of a property, the property value is returned. If it is the id of an element in
296      * the array, that element is returned. Finally, if it is the name of an element or
297      * elements in the array, then all those elements are returned. Otherwise,
298      * {@link #NOT_FOUND} is returned.
299      * {@inheritDoc}
300      */
301     @Override
302     protected Object getWithPreemption(final String name) {
303         // Test to see if we are trying to get the length of this collection?
304         // If so return NOT_FOUND here to let the property be retrieved using the prototype
305         if (/*xpath_ == null || */"length".equals(name)) {
306             return NOT_FOUND;
307         }
308 
309         final List<DomNode> elements = getElements();
310 
311         // See if there is an element in the element array with the specified id.
312         final List<DomNode> matchingElements = new ArrayList<>();
313 
314         for (final DomNode next : elements) {
315             if (next instanceof DomElement) {
316                 final String id = ((DomElement) next).getId();
317                 if (name.equals(id)) {
318                     matchingElements.add(next);
319                 }
320             }
321         }
322 
323         if (matchingElements.size() == 1) {
324             return getScriptableForElement(matchingElements.get(0));
325         }
326         else if (!matchingElements.isEmpty()) {
327             final AbstractList collection = create(getDomNodeOrDie(), matchingElements);
328             collection.setAvoidObjectDetection(true);
329             return collection;
330         }
331 
332         // no element found by id, let's search by name
333         return getWithPreemptionByName(name, elements);
334     }
335 
336     /**
337      * Constructs a new instance with an initial cache value.
338      * @param parentScope the parent scope, on which we listen for changes
339      * @param initialElements the initial content for the cache
340      * @return the newly created instance
341      */
342     protected AbstractList create(final DomNode parentScope, final List<DomNode> initialElements) {
343         return new AbstractList(parentScope, initialElements);
344     }
345 
346     /**
347      * Helper for {@link #getWithPreemption(String)} when finding by id doesn't get results.
348      * @param name the property name
349      * @param elements the children elements.
350      * @return {@link Scriptable#NOT_FOUND} if not found
351      */
352     protected Object getWithPreemptionByName(final String name, final List<DomNode> elements) {
353         final List<DomNode> matchingElements = new ArrayList<>();
354         for (final DomNode next : elements) {
355             if (next instanceof DomElement) {
356                 final String nodeName = ((DomElement) next).getAttribute("name");
357                 if (name.equals(nodeName)) {
358                     matchingElements.add(next);
359                 }
360             }
361         }
362 
363         if (matchingElements.isEmpty()) {
364             return NOT_FOUND;
365         }
366         else if (matchingElements.size() == 1) {
367             return getScriptableForElement(matchingElements.get(0));
368         }
369 
370         // many elements => build a sub collection
371         final DomNode domNode = getDomNodeOrNull();
372         final AbstractList collection = create(domNode, matchingElements);
373         collection.setAvoidObjectDetection(true);
374         return collection;
375     }
376 
377     /**
378      * Returns the length.
379      * @return the length
380      */
381     @JsxGetter
382     public final int getLength() {
383         return getElements().size();
384     }
385 
386     /**
387      * Returns the item or items corresponding to the specified index or key.
388      * @param index the index or key corresponding to the element or elements to return
389      * @return the element or elements corresponding to the specified index or key
390      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536460.aspx">MSDN doc</a>
391      */
392     @JsxFunction
393     public Object item(final Object index) {
394         final Object object = getIt(index);
395         if (object == NOT_FOUND) {
396             if (getBrowserVersion().hasFeature(HTMLCOLLECTION_NULL_IF_ITEM_NOT_FOUND)) {
397                 return null;
398             }
399             return Undefined.instance;
400         }
401         return object;
402     }
403 
404     /**
405      * {@inheritDoc}
406      */
407     @Override
408     public String toString() {
409         return getClass().getSimpleName() + " for " + getDomNodeOrNull();
410     }
411 
412     /**
413      * Called for the js "==".
414      * {@inheritDoc}
415      */
416     @Override
417     protected Object equivalentValues(final Object other) {
418         if (other == this) {
419             return Boolean.TRUE;
420         }
421         else if (other instanceof AbstractList) {
422             final AbstractList otherArray = (AbstractList) other;
423             final DomNode domNode = getDomNodeOrNull();
424             final DomNode domNodeOther = otherArray.getDomNodeOrNull();
425             if (getClass() == other.getClass()
426                     && domNode == domNodeOther
427                     && getElements().equals(otherArray.getElements())) {
428                 return Boolean.TRUE;
429             }
430             return NOT_FOUND;
431         }
432 
433         return super.equivalentValues(other);
434     }
435 
436     /**
437      * {@inheritDoc}
438      */
439     @Override
440     public boolean has(final int index, final Scriptable start) {
441         return index >= 0 && index < getElements().size();
442     }
443 
444     /**
445      * {@inheritDoc}
446      */
447     @Override
448     public boolean has(final String name, final Scriptable start) {
449         // let's Rhino work normally if current instance is the prototype
450         if (isPrototype()) {
451             return super.has(name, start);
452         }
453 
454         try {
455             return has(Integer.parseInt(name), start);
456         }
457         catch (final NumberFormatException e) {
458             // Ignore.
459         }
460 
461         if ("length".equals(name)) {
462             return true;
463         }
464         final BrowserVersion browserVersion = getBrowserVersion();
465         if (browserVersion.hasFeature(JS_NODE_LIST_ENUMERATE_FUNCTIONS)) {
466             final JavaScriptConfiguration jsConfig = getWindow().getWebWindow().getWebClient()
467                     .getJavaScriptEngine().getJavaScriptConfiguration();
468             if (jsConfig.getClassConfiguration(getClassName()).getFunctionKeys().contains(name)) {
469                 return true;
470             }
471         }
472 
473         if (browserVersion.hasFeature(JS_NODE_LIST_ENUMERATE_CHILDREN)) {
474             for (final Object next : getElements()) {
475                 if (next instanceof DomElement) {
476                     final DomElement element = (DomElement) next;
477                     if (name.equals(element.getAttribute("name"))) {
478                         return true;
479                     }
480 
481                     final String id = element.getId();
482                     if (name.equals(id)) {
483                         return true;
484                     }
485                 }
486             }
487         }
488         return getWithPreemption(name) != NOT_FOUND;
489     }
490 
491     /**
492      * {@inheritDoc}.
493      */
494     @Override
495     public Object[] getIds() {
496         // let's Rhino work normally if current instance is the prototype
497         if (isPrototype()) {
498             return super.getIds();
499         }
500 
501         final List<String> idList = new ArrayList<>();
502         final List<DomNode> elements = getElements();
503 
504         final BrowserVersion browserVersion = getBrowserVersion();
505         if (browserVersion.hasFeature(JS_NODE_LIST_ENUMERATE_FUNCTIONS)) {
506             final int length = elements.size();
507             for (int i = 0; i < length; i++) {
508                 idList.add(Integer.toString(i));
509             }
510 
511             idList.add("length");
512             final JavaScriptConfiguration jsConfig = getWindow().getWebWindow().getWebClient()
513                 .getJavaScriptEngine().getJavaScriptConfiguration();
514             for (final String name : jsConfig.getClassConfiguration(getClassName()).getFunctionKeys()) {
515                 idList.add(name);
516             }
517         }
518         else {
519             idList.add("length");
520         }
521         if (browserVersion.hasFeature(JS_NODE_LIST_ENUMERATE_CHILDREN)) {
522             addElementIds(idList, elements);
523         }
524 
525         return idList.toArray();
526     }
527 
528     private boolean isPrototype() {
529         return !(getPrototype() instanceof AbstractList);
530     }
531 
532     /**
533      * Adds the ids of the collection's elements to the idList.
534      * @param idList the list to add the ids to
535      * @param elements the collection's elements
536      */
537     protected void addElementIds(final List<String> idList, final List<DomNode> elements) {
538         int index = 0;
539         for (final DomNode next : elements) {
540             if (next instanceof DomElement) {
541                 final DomElement element = (DomElement) next;
542                 final String name = element.getAttribute("name");
543                 if (name != DomElement.ATTRIBUTE_NOT_DEFINED) {
544                     idList.add(name);
545                 }
546                 final String id = element.getId();
547                 if (id != DomElement.ATTRIBUTE_NOT_DEFINED) {
548                     idList.add(id);
549                 }
550             }
551             if (!getBrowserVersion().hasFeature(JS_NODE_LIST_ENUMERATE_FUNCTIONS)) {
552                 idList.add(Integer.toString(index));
553             }
554             index++;
555         }
556     }
557 
558     private static final class DomHtmlAttributeChangeListenerImpl
559                                     implements DomChangeListener, HtmlAttributeChangeListener {
560 
561         private transient WeakReference<AbstractList> nodeList_;
562 
563         private DomHtmlAttributeChangeListenerImpl(final AbstractList nodeList) {
564             super();
565 
566             nodeList_ = new WeakReference<>(nodeList);
567         }
568 
569         /**
570          * {@inheritDoc}
571          */
572         @Override
573         public void nodeAdded(final DomChangeEvent event) {
574             clearCache();
575         }
576 
577         /**
578          * {@inheritDoc}
579          */
580         @Override
581         public void nodeDeleted(final DomChangeEvent event) {
582             clearCache();
583         }
584 
585         /**
586          * {@inheritDoc}
587          */
588         @Override
589         public void attributeAdded(final HtmlAttributeChangeEvent event) {
590             handleChangeOnCache(event);
591         }
592 
593         /**
594          * {@inheritDoc}
595          */
596         @Override
597         public void attributeRemoved(final HtmlAttributeChangeEvent event) {
598             handleChangeOnCache(event);
599         }
600 
601         /**
602          * {@inheritDoc}
603          */
604         @Override
605         public void attributeReplaced(final HtmlAttributeChangeEvent event) {
606             final AbstractList nodes = nodeList_.get();
607             if (null == nodes) {
608                 return;
609             }
610             if (nodes.attributeChangeSensitive_) {
611                 handleChangeOnCache(event);
612             }
613         }
614 
615         private void handleChangeOnCache(final HtmlAttributeChangeEvent event) {
616             final AbstractList nodes = nodeList_.get();
617             if (null == nodes) {
618                 return;
619             }
620 
621             final EffectOnCache effectOnCache = nodes.getEffectOnCache(event);
622             if (EffectOnCache.NONE == effectOnCache) {
623                 return;
624             }
625             if (EffectOnCache.RESET == effectOnCache) {
626                 clearCache();
627             }
628         }
629 
630         private void clearCache() {
631             final AbstractList nodes = nodeList_.get();
632             if (null != nodes) {
633                 nodes.cachedElements_ = null;
634             }
635         }
636     }
637 
638     /**
639      * Gets the effect of the change on an attribute of the reference node
640      * on this collection's cache.
641      * @param event the change event
642      * @return the effect on cache
643      */
644     protected EffectOnCache getEffectOnCache(final HtmlAttributeChangeEvent event) {
645         return EffectOnCache.RESET;
646     }
647 
648     /**
649      * Gets the scriptable for the provided element that may already be the right scriptable.
650      * @param object the object for which to get the scriptable
651      * @return the scriptable
652      */
653     protected Scriptable getScriptableForElement(final Object object) {
654         if (object instanceof Scriptable) {
655             return (Scriptable) object;
656         }
657         return getScriptableFor(object);
658     }
659 }