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