View Javadoc
1   /*
2    * Copyright (c) 2002-2025 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.javascript.host.dom;
16  
17  import java.io.Serializable;
18  import java.lang.ref.WeakReference;
19  import java.lang.reflect.Method;
20  import java.util.ArrayList;
21  import java.util.List;
22  import java.util.function.Function;
23  import java.util.function.Predicate;
24  import java.util.function.Supplier;
25  
26  import org.htmlunit.corejs.javascript.ExternalArrayData;
27  import org.htmlunit.corejs.javascript.Scriptable;
28  import org.htmlunit.html.DomChangeEvent;
29  import org.htmlunit.html.DomChangeListener;
30  import org.htmlunit.html.DomElement;
31  import org.htmlunit.html.DomNode;
32  import org.htmlunit.html.HtmlAttributeChangeEvent;
33  import org.htmlunit.html.HtmlAttributeChangeListener;
34  import org.htmlunit.html.HtmlElement;
35  import org.htmlunit.html.HtmlPage;
36  import org.htmlunit.javascript.HtmlUnitScriptable;
37  
38  /**
39   * The parent class of {@link NodeList} and {@link org.htmlunit.javascript.host.html.HTMLCollection}.
40   *
41   * @author Daniel Gredler
42   * @author Marc Guillemot
43   * @author Chris Erskine
44   * @author Ahmed Ashour
45   * @author Frank Danek
46   * @author Ronald Brill
47   */
48  public class AbstractList extends HtmlUnitScriptable implements ExternalArrayData {
49  
50      /**
51       * Cache effect of some changes.
52       */
53      public enum EffectOnCache {
54          /** No effect, cache is still valid. */
55          NONE,
56          /** Cache is not valid anymore and should be reset. */
57          RESET
58      }
59  
60      private boolean avoidObjectDetection_;
61  
62      private boolean attributeChangeSensitive_;
63  
64      /**
65       * Cache collection elements when possible, so as to avoid expensive XPath expression evaluations.
66       */
67      private List<DomNode> cachedElements_;
68  
69      private boolean listenerRegistered_;
70  
71      private Function<HtmlAttributeChangeEvent, EffectOnCache> effectOnCacheFunction_ =
72              (Function<HtmlAttributeChangeEvent, EffectOnCache> & Serializable) event -> EffectOnCache.RESET;
73  
74      private Predicate<DomNode> isMatchingPredicate_ = (Predicate<DomNode> & Serializable) domNode -> false;
75  
76      private Supplier<List<DomNode>> elementsSupplier_ =
77              (Supplier<List<DomNode>> & Serializable)
78                  () -> {
79                      final List<DomNode> response = new ArrayList<>();
80                      final DomNode domNode = getDomNodeOrNull();
81                      if (domNode == null) {
82                          return response;
83                      }
84                      for (final DomNode desc : domNode.getDescendants()) {
85                          if (desc instanceof DomElement && isMatchingPredicate_.test(desc)) {
86                              response.add(desc);
87                          }
88                      }
89                      return response;
90                  };
91  
92      /**
93       * Creates an instance.
94       */
95      public AbstractList() {
96          super();
97      }
98  
99      /**
100      * Creates an instance.
101      *
102      * @param domNode the {@link DomNode}
103      * @param attributeChangeSensitive indicates if the content of the collection may change when an attribute
104      *        of a descendant node of parentScope changes (attribute added, modified or removed)
105      * @param initialElements the initial content for the cache
106      */
107     protected AbstractList(final DomNode domNode, final boolean attributeChangeSensitive,
108             final List<DomNode> initialElements) {
109         super();
110         if (domNode != null) {
111             setDomNode(domNode, false);
112             final HtmlUnitScriptable parentScope = domNode.getScriptableObject();
113             if (parentScope != null) {
114                 setParentScope(parentScope);
115             }
116             setPrototype(getPrototype(getClass()));
117         }
118         attributeChangeSensitive_ = attributeChangeSensitive;
119         cachedElements_ = initialElements;
120         if (initialElements != null) {
121             registerListener();
122         }
123         setExternalArrayData(this);
124     }
125 
126     /**
127      * Only needed to make collections like <code>document.all</code> available but "invisible" when simulating Firefox.
128      * {@inheritDoc}
129      */
130     @Override
131     public boolean avoidObjectDetection() {
132         return avoidObjectDetection_;
133     }
134 
135     /**
136      * @param newValue the new value
137      */
138     public void setAvoidObjectDetection(final boolean newValue) {
139         avoidObjectDetection_ = newValue;
140     }
141 
142     /**
143      * @param effectOnCacheFunction the new function
144      */
145     public void setEffectOnCacheFunction(
146             final Function<HtmlAttributeChangeEvent, EffectOnCache> effectOnCacheFunction) {
147         if (effectOnCacheFunction == null) {
148             throw new NullPointerException("EffectOnCacheFunction can't be null");
149         }
150         effectOnCacheFunction_ = effectOnCacheFunction;
151     }
152 
153     /**
154      * @return elementSupplier
155      */
156     protected Supplier<List<DomNode>> getElementSupplier() {
157         return elementsSupplier_;
158     }
159 
160     /**
161      * Returns the elements whose associated host objects are available through this collection.
162      * @param elementsSupplier the new supplier
163      */
164     public void setElementsSupplier(final Supplier<List<DomNode>> elementsSupplier) {
165         if (elementsSupplier == null) {
166             throw new NullPointerException("ElementsSupplier can't be null");
167         }
168         elementsSupplier_ = elementsSupplier;
169     }
170 
171     /**
172      * @return isMatchingPredicate
173      */
174     protected Predicate<DomNode> getIsMatchingPredicate() {
175         return isMatchingPredicate_;
176     }
177 
178     /**
179      * Indicates if the node should belong to the collection.
180      * @param isMatchingPredicate the new predicate
181      */
182     public void setIsMatchingPredicate(final Predicate<DomNode> isMatchingPredicate) {
183         if (isMatchingPredicate == null) {
184             throw new NullPointerException("IsMatchingPredicate can't be null");
185         }
186         isMatchingPredicate_ = isMatchingPredicate;
187     }
188 
189     /**
190      * Private helper that retrieves the item or items corresponding to the specified
191      * index or key.
192      * @param o the index or key corresponding to the element or elements to return
193      * @return the element or elements corresponding to the specified index or key
194      */
195     protected Object getIt(final Object o) {
196         if (o instanceof Number) {
197             final Number n = (Number) o;
198             final int i = n.intValue();
199             return get(i, this);
200         }
201         final String key = String.valueOf(o);
202         return get(key, this);
203     }
204 
205     @Override
206     public 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 = elementsSupplier_.get();
230             }
231             cachedElements_ = cachedElements;
232         }
233         registerListener();
234 
235         // maybe the cache was cleared in between
236         // then this returns the old state and never null
237         return cachedElements;
238     }
239 
240     private void registerListener() {
241         if (!listenerRegistered_) {
242             final DomNode domNode = getDomNodeOrNull();
243             if (domNode != null) {
244                 final DomHtmlAttributeChangeListenerImpl listener = new DomHtmlAttributeChangeListenerImpl(this);
245                 domNode.addDomChangeListener(listener);
246                 if (attributeChangeSensitive_) {
247                     if (domNode instanceof HtmlElement) {
248                         ((HtmlElement) domNode).addHtmlAttributeChangeListener(listener);
249                     }
250                     else if (domNode instanceof HtmlPage) {
251                         ((HtmlPage) domNode).addHtmlAttributeChangeListener(listener);
252                     }
253                 }
254                 listenerRegistered_ = true;
255             }
256         }
257     }
258 
259     /**
260      * Returns the element or elements that match the specified key. If it is the name
261      * of a property, the property value is returned. If it is the id of an element in
262      * the array, that element is returned. Finally, if it is the name of an element or
263      * elements in the array, then all those elements are returned. Otherwise,
264      * {@link #NOT_FOUND} is returned.
265      * {@inheritDoc}
266      */
267     @Override
268     protected Object getWithPreemption(final String name) {
269         // Test to see if we are trying to get the length of this collection?
270         // If so return NOT_FOUND here to let the property be retrieved using the prototype
271         if ("length".equals(name)) {
272             return NOT_FOUND;
273         }
274 
275         final List<DomNode> elements = getElements();
276 
277         // See if there is an element in the element array with the specified id.
278         final List<DomNode> matchingElements = new ArrayList<>();
279 
280         for (final DomNode next : elements) {
281             if (next instanceof DomElement) {
282                 final String id = ((DomElement) next).getId();
283                 if (name.equals(id)) {
284                     matchingElements.add(next);
285                 }
286             }
287         }
288 
289         if (matchingElements.size() == 1) {
290             return getScriptableForElement(matchingElements.get(0));
291         }
292         else if (!matchingElements.isEmpty()) {
293             final AbstractList collection = create(getDomNodeOrDie(), matchingElements);
294             collection.setAvoidObjectDetection(true);
295             return collection;
296         }
297 
298         // no element found by id, let's search by name
299         return getWithPreemptionByName(name, elements);
300     }
301 
302     /**
303      * Constructs a new instance with an initial cache value.
304      * @param parentScope the parent scope, on which we listen for changes
305      * @param initialElements the initial content for the cache
306      * @return the newly created instance
307      */
308     protected AbstractList create(final DomNode parentScope, final List<DomNode> initialElements) {
309         throw new IllegalAccessError("Creation of AbstractListInstances is not allowed.");
310     }
311 
312     /**
313      * Helper for {@link #getWithPreemption(String)} when finding by id doesn't get results.
314      * @param name the property name
315      * @param elements the children elements.
316      * @return {@link Scriptable#NOT_FOUND} if not found
317      */
318     protected Object getWithPreemptionByName(final String name, final List<DomNode> elements) {
319         final List<DomNode> matchingElements = new ArrayList<>();
320         for (final DomNode next : elements) {
321             if (next instanceof DomElement) {
322                 final String nodeName = ((DomElement) next).getAttributeDirect(DomElement.NAME_ATTRIBUTE);
323                 if (name.equals(nodeName)) {
324                     matchingElements.add(next);
325                 }
326             }
327         }
328 
329         if (matchingElements.isEmpty()) {
330             return NOT_FOUND;
331         }
332         else if (matchingElements.size() == 1) {
333             return getScriptableForElement(matchingElements.get(0));
334         }
335 
336         // many elements => build a sub collection
337         final DomNode domNode = getDomNodeOrNull();
338         final AbstractList collection = create(domNode, matchingElements);
339         collection.setAvoidObjectDetection(true);
340         return collection;
341     }
342 
343     /**
344      * Returns the length.
345      * @return the length
346      */
347     public int getLength() {
348         return getElements().size();
349     }
350 
351     /**
352      * {@inheritDoc}
353      */
354     @Override
355     public String toString() {
356         return getClass().getSimpleName() + " for " + getDomNodeOrNull();
357     }
358 
359     /**
360      * Called for the js "==".
361      * {@inheritDoc}
362      */
363     @Override
364     protected Object equivalentValues(final Object other) {
365         if (other == this) {
366             return Boolean.TRUE;
367         }
368         else if (other instanceof AbstractList) {
369             final AbstractList otherArray = (AbstractList) other;
370             final DomNode domNode = getDomNodeOrNull();
371             final DomNode domNodeOther = otherArray.getDomNodeOrNull();
372             if (getClass() == other.getClass()
373                     && domNode == domNodeOther
374                     && getElements().equals(otherArray.getElements())) {
375                 return Boolean.TRUE;
376             }
377             return NOT_FOUND;
378         }
379 
380         return super.equivalentValues(other);
381     }
382 
383     private static final class DomHtmlAttributeChangeListenerImpl
384                                     implements DomChangeListener, HtmlAttributeChangeListener {
385 
386         private final transient WeakReference<AbstractList> nodeList_;
387 
388         DomHtmlAttributeChangeListenerImpl(final AbstractList nodeList) {
389             super();
390 
391             nodeList_ = new WeakReference<>(nodeList);
392         }
393 
394         /**
395          * {@inheritDoc}
396          */
397         @Override
398         public void nodeAdded(final DomChangeEvent event) {
399             clearCache();
400         }
401 
402         /**
403          * {@inheritDoc}
404          */
405         @Override
406         public void nodeDeleted(final DomChangeEvent event) {
407             clearCache();
408         }
409 
410         /**
411          * {@inheritDoc}
412          */
413         @Override
414         public void attributeAdded(final HtmlAttributeChangeEvent event) {
415             handleChangeOnCache(event);
416         }
417 
418         /**
419          * {@inheritDoc}
420          */
421         @Override
422         public void attributeRemoved(final HtmlAttributeChangeEvent event) {
423             handleChangeOnCache(event);
424         }
425 
426         /**
427          * {@inheritDoc}
428          */
429         @Override
430         public void attributeReplaced(final HtmlAttributeChangeEvent event) {
431             final AbstractList nodes = nodeList_.get();
432             if (null == nodes) {
433                 return;
434             }
435             if (nodes.attributeChangeSensitive_) {
436                 handleChangeOnCache(event);
437             }
438         }
439 
440         private void handleChangeOnCache(final HtmlAttributeChangeEvent event) {
441             final AbstractList nodes = nodeList_.get();
442             if (null == nodes) {
443                 return;
444             }
445 
446             final EffectOnCache effectOnCache = nodes.effectOnCacheFunction_.apply(event);
447             if (EffectOnCache.NONE == effectOnCache) {
448                 return;
449             }
450             if (EffectOnCache.RESET == effectOnCache) {
451                 clearCache();
452             }
453         }
454 
455         private void clearCache() {
456             final AbstractList nodes = nodeList_.get();
457             if (null != nodes) {
458                 nodes.cachedElements_ = null;
459             }
460         }
461     }
462 
463     /**
464      * Gets the scriptable for the provided element that may already be the right scriptable.
465      * @param object the object for which to get the scriptable
466      * @return the scriptable
467      */
468     protected Scriptable getScriptableForElement(final Object object) {
469         if (object instanceof Scriptable) {
470             return (Scriptable) object;
471         }
472         return getScriptableFor(object);
473     }
474 
475     /**
476      * {@inheritDoc}
477      */
478     @Override
479     public void defineProperty(final String propertyName, final Object delegateTo,
480             final Method getter, final Method setter, final int attributes) {
481         // length is defined on the prototype, don't define it again
482         if ("length".equals(propertyName) && getPrototype() != null) {
483             return;
484         }
485 
486         super.defineProperty(propertyName, delegateTo, getter, setter, attributes);
487     }
488 
489     @Override
490     public Object getArrayElement(final int index) {
491         final List<DomNode> elements = getElements();
492         if (index >= 0 && index < elements.size()) {
493             return getScriptableForElement(elements.get(index));
494         }
495         return NOT_FOUND;
496     }
497 
498     @Override
499     public void setArrayElement(final int index, final Object value) {
500         // ignore
501     }
502 
503     @Override
504     public int getArrayLength() {
505         return getElements().size();
506     }
507 }