1   
2   
3   
4   
5   
6   
7   
8   
9   
10  
11  
12  
13  
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  
40  
41  
42  
43  
44  
45  
46  
47  
48  public class AbstractList extends HtmlUnitScriptable implements ExternalArrayData {
49  
50      
51  
52  
53      public enum EffectOnCache {
54          
55          NONE,
56          
57          RESET
58      }
59  
60      private boolean avoidObjectDetection_;
61  
62      private boolean attributeChangeSensitive_;
63  
64      
65  
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  
94  
95      public AbstractList() {
96          super();
97      }
98  
99      
100 
101 
102 
103 
104 
105 
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 
128 
129 
130     @Override
131     public boolean avoidObjectDetection() {
132         return avoidObjectDetection_;
133     }
134 
135     
136 
137 
138     public void setAvoidObjectDetection(final boolean newValue) {
139         avoidObjectDetection_ = newValue;
140     }
141 
142     
143 
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 
155 
156     protected Supplier<List<DomNode>> getElementSupplier() {
157         return elementsSupplier_;
158     }
159 
160     
161 
162 
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 
173 
174     protected Predicate<DomNode> getIsMatchingPredicate() {
175         return isMatchingPredicate_;
176     }
177 
178     
179 
180 
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 
191 
192 
193 
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 
218 
219 
220     public List<DomNode> getElements() {
221         
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         
236         
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 
261 
262 
263 
264 
265 
266 
267     @Override
268     protected Object getWithPreemption(final String name) {
269         
270         
271         if ("length".equals(name)) {
272             return NOT_FOUND;
273         }
274 
275         final List<DomNode> elements = getElements();
276 
277         
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         
299         return getWithPreemptionByName(name, elements);
300     }
301 
302     
303 
304 
305 
306 
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 
314 
315 
316 
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         
337         final DomNode domNode = getDomNodeOrNull();
338         final AbstractList collection = create(domNode, matchingElements);
339         collection.setAvoidObjectDetection(true);
340         return collection;
341     }
342 
343     
344 
345 
346 
347     public int getLength() {
348         return getElements().size();
349     }
350 
351     
352 
353 
354     @Override
355     public String toString() {
356         return getClass().getSimpleName() + " for " + getDomNodeOrNull();
357     }
358 
359     
360 
361 
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 
396 
397         @Override
398         public void nodeAdded(final DomChangeEvent event) {
399             clearCache();
400         }
401 
402         
403 
404 
405         @Override
406         public void nodeDeleted(final DomChangeEvent event) {
407             clearCache();
408         }
409 
410         
411 
412 
413         @Override
414         public void attributeAdded(final HtmlAttributeChangeEvent event) {
415             handleChangeOnCache(event);
416         }
417 
418         
419 
420 
421         @Override
422         public void attributeRemoved(final HtmlAttributeChangeEvent event) {
423             handleChangeOnCache(event);
424         }
425 
426         
427 
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 
465 
466 
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 
477 
478     @Override
479     public void defineProperty(final String propertyName, final Object delegateTo,
480             final Method getter, final Method setter, final int attributes) {
481         
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         
501     }
502 
503     @Override
504     public int getArrayLength() {
505         return getElements().size();
506     }
507 }