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