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.corejs.javascript.VarScope;
29 import org.htmlunit.html.DomChangeEvent;
30 import org.htmlunit.html.DomChangeListener;
31 import org.htmlunit.html.DomElement;
32 import org.htmlunit.html.DomNode;
33 import org.htmlunit.html.HtmlAttributeChangeEvent;
34 import org.htmlunit.html.HtmlAttributeChangeListener;
35 import org.htmlunit.html.HtmlElement;
36 import org.htmlunit.html.HtmlPage;
37 import org.htmlunit.javascript.HtmlUnitScriptable;
38
39
40
41
42
43
44
45
46
47
48
49 public class AbstractList extends HtmlUnitScriptable implements ExternalArrayData {
50
51
52
53
54 public enum EffectOnCache {
55
56 NONE,
57
58 RESET
59 }
60
61 private boolean avoidObjectDetection_;
62
63 private boolean attributeChangeSensitive_;
64
65
66
67
68 private List<DomNode> cachedElements_;
69
70 private boolean listenerRegistered_;
71
72 private Function<HtmlAttributeChangeEvent, EffectOnCache> effectOnCacheFunction_ =
73 (Function<HtmlAttributeChangeEvent, EffectOnCache> & Serializable) event -> EffectOnCache.RESET;
74
75 private Predicate<DomNode> isMatchingPredicate_ = (Predicate<DomNode> & Serializable) domNode -> false;
76
77 private Supplier<List<DomNode>> elementsSupplier_ =
78 (Supplier<List<DomNode>> & Serializable)
79 () -> {
80 final List<DomNode> response = new ArrayList<>();
81 final DomNode domNode = getDomNodeOrNull();
82 if (domNode == null) {
83 return response;
84 }
85 for (final DomNode desc : domNode.getDescendants()) {
86 if (desc instanceof DomElement && isMatchingPredicate_.test(desc)) {
87 response.add(desc);
88 }
89 }
90 return response;
91 };
92
93
94
95
96 public AbstractList() {
97 super();
98 }
99
100
101
102
103
104
105
106
107
108 protected AbstractList(final DomNode domNode, final boolean attributeChangeSensitive,
109 final List<DomNode> initialElements) {
110 super();
111 if (domNode != null) {
112 setDomNode(domNode, false);
113 setParentScope(domNode.getScriptableObject().getParentScope());
114 setPrototype(getPrototype(getClass()));
115 }
116 attributeChangeSensitive_ = attributeChangeSensitive;
117 cachedElements_ = initialElements;
118 if (initialElements != null) {
119 registerListener();
120 }
121 setExternalArrayData(getParentScope(), this);
122 }
123
124
125
126
127
128 @Override
129 public boolean avoidObjectDetection() {
130 return avoidObjectDetection_;
131 }
132
133
134
135
136 public void setAvoidObjectDetection(final boolean newValue) {
137 avoidObjectDetection_ = newValue;
138 }
139
140
141
142
143 public void setEffectOnCacheFunction(
144 final Function<HtmlAttributeChangeEvent, EffectOnCache> effectOnCacheFunction) {
145 if (effectOnCacheFunction == null) {
146 throw new NullPointerException("EffectOnCacheFunction can't be null");
147 }
148 effectOnCacheFunction_ = effectOnCacheFunction;
149 }
150
151
152
153
154 protected Supplier<List<DomNode>> getElementSupplier() {
155 return elementsSupplier_;
156 }
157
158
159
160
161
162 public void setElementsSupplier(final Supplier<List<DomNode>> elementsSupplier) {
163 if (elementsSupplier == null) {
164 throw new NullPointerException("ElementsSupplier can't be null");
165 }
166 elementsSupplier_ = elementsSupplier;
167 }
168
169
170
171
172 protected Predicate<DomNode> getIsMatchingPredicate() {
173 return isMatchingPredicate_;
174 }
175
176
177
178
179
180 public void setIsMatchingPredicate(final Predicate<DomNode> isMatchingPredicate) {
181 if (isMatchingPredicate == null) {
182 throw new NullPointerException("IsMatchingPredicate can't be null");
183 }
184 isMatchingPredicate_ = isMatchingPredicate;
185 }
186
187
188
189
190
191
192
193 protected Object getIt(final Object o) {
194 if (o instanceof Number n) {
195 final int i = n.intValue();
196 return get(i, this);
197 }
198 final String key = String.valueOf(o);
199 return get(key, this);
200 }
201
202 @Override
203 public void setDomNode(final DomNode domNode, final boolean assignScriptObject) {
204 final DomNode oldDomNode = getDomNodeOrNull();
205
206 super.setDomNode(domNode, assignScriptObject);
207
208 if (oldDomNode != domNode) {
209 listenerRegistered_ = false;
210 }
211 }
212
213
214
215
216
217 public List<DomNode> getElements() {
218
219 List<DomNode> cachedElements = cachedElements_;
220
221 if (cachedElements == null) {
222 if (getParentScope() == null) {
223 cachedElements = new ArrayList<>();
224 }
225 else {
226 cachedElements = elementsSupplier_.get();
227 }
228 cachedElements_ = cachedElements;
229 }
230 registerListener();
231
232
233
234 return cachedElements;
235 }
236
237 private void registerListener() {
238 if (!listenerRegistered_) {
239 final DomNode domNode = getDomNodeOrNull();
240 if (domNode != null) {
241 final DomHtmlAttributeChangeListenerImpl listener = new DomHtmlAttributeChangeListenerImpl(this);
242 domNode.addDomChangeListener(listener);
243 if (attributeChangeSensitive_) {
244 if (domNode instanceof HtmlElement element) {
245 element.addHtmlAttributeChangeListener(listener);
246 }
247 else if (domNode instanceof HtmlPage page) {
248 page.addHtmlAttributeChangeListener(listener);
249 }
250 }
251 listenerRegistered_ = true;
252 }
253 }
254 }
255
256
257
258
259
260
261
262
263
264 @Override
265 protected Object getWithPreemption(final String name) {
266
267
268 if ("length".equals(name)) {
269 return NOT_FOUND;
270 }
271
272 final List<DomNode> elements = getElements();
273
274
275 final List<DomNode> matchingElements = new ArrayList<>();
276
277 for (final DomNode next : elements) {
278 if (next instanceof DomElement element) {
279 final String id = element.getId();
280 if (name.equals(id)) {
281 matchingElements.add(next);
282 }
283 }
284 }
285
286 if (matchingElements.size() == 1) {
287 return getScriptableForElement(matchingElements.get(0));
288 }
289 else if (!matchingElements.isEmpty()) {
290 final AbstractList collection = create(getDomNodeOrDie(), matchingElements);
291 collection.setAvoidObjectDetection(true);
292 return collection;
293 }
294
295
296 return getWithPreemptionByName(name, elements);
297 }
298
299
300
301
302
303
304
305 protected AbstractList create(final DomNode parentScope, final List<DomNode> initialElements) {
306 throw new IllegalAccessError("Creation of AbstractListInstances is not allowed.");
307 }
308
309
310
311
312
313
314
315 protected Object getWithPreemptionByName(final String name, final List<DomNode> elements) {
316 final List<DomNode> matchingElements = new ArrayList<>();
317 for (final DomNode next : elements) {
318 if (next instanceof DomElement element) {
319 final String nodeName = element.getAttributeDirect(DomElement.NAME_ATTRIBUTE);
320 if (name.equals(nodeName)) {
321 matchingElements.add(next);
322 }
323 }
324 }
325
326 if (matchingElements.isEmpty()) {
327 return NOT_FOUND;
328 }
329 else if (matchingElements.size() == 1) {
330 return getScriptableForElement(matchingElements.get(0));
331 }
332
333
334 final DomNode domNode = getDomNodeOrNull();
335 final AbstractList collection = create(domNode, matchingElements);
336 collection.setAvoidObjectDetection(true);
337 return collection;
338 }
339
340
341
342
343
344 public int getLength() {
345 return getElements().size();
346 }
347
348
349
350
351 @Override
352 public String toString() {
353 return getClass().getSimpleName() + " for " + getDomNodeOrNull();
354 }
355
356
357
358
359
360 @Override
361 protected Object equivalentValues(final Object other) {
362 if (other == this) {
363 return Boolean.TRUE;
364 }
365 else if (other instanceof AbstractList otherArray) {
366 final DomNode domNode = getDomNodeOrNull();
367 final DomNode domNodeOther = otherArray.getDomNodeOrNull();
368 if (getClass() == other.getClass()
369 && domNode == domNodeOther
370 && getElements().equals(otherArray.getElements())) {
371 return Boolean.TRUE;
372 }
373 return NOT_FOUND;
374 }
375
376 return super.equivalentValues(other);
377 }
378
379 private static final class DomHtmlAttributeChangeListenerImpl
380 implements DomChangeListener, HtmlAttributeChangeListener {
381
382 private final transient WeakReference<AbstractList> nodeList_;
383
384 DomHtmlAttributeChangeListenerImpl(final AbstractList nodeList) {
385 super();
386
387 nodeList_ = new WeakReference<>(nodeList);
388 }
389
390
391
392
393 @Override
394 public void nodeAdded(final DomChangeEvent event) {
395 clearCache();
396 }
397
398
399
400
401 @Override
402 public void nodeDeleted(final DomChangeEvent event) {
403 clearCache();
404 }
405
406
407
408
409 @Override
410 public void attributeAdded(final HtmlAttributeChangeEvent event) {
411 handleChangeOnCache(event);
412 }
413
414
415
416
417 @Override
418 public void attributeRemoved(final HtmlAttributeChangeEvent event) {
419 handleChangeOnCache(event);
420 }
421
422
423
424
425 @Override
426 public void attributeReplaced(final HtmlAttributeChangeEvent event) {
427 final AbstractList nodes = nodeList_.get();
428 if (null == nodes) {
429 return;
430 }
431 if (nodes.attributeChangeSensitive_) {
432 handleChangeOnCache(event);
433 }
434 }
435
436 private void handleChangeOnCache(final HtmlAttributeChangeEvent event) {
437 final AbstractList nodes = nodeList_.get();
438 if (null == nodes) {
439 return;
440 }
441
442 final EffectOnCache effectOnCache = nodes.effectOnCacheFunction_.apply(event);
443 if (EffectOnCache.NONE == effectOnCache) {
444 return;
445 }
446 if (EffectOnCache.RESET == effectOnCache) {
447 clearCache();
448 }
449 }
450
451 private void clearCache() {
452 final AbstractList nodes = nodeList_.get();
453 if (null != nodes) {
454 nodes.cachedElements_ = null;
455 }
456 }
457 }
458
459
460
461
462
463
464 protected Scriptable getScriptableForElement(final Object object) {
465 if (object instanceof Scriptable scriptable) {
466 return scriptable;
467 }
468 return getScriptableFor(object);
469 }
470
471
472
473
474 @Override
475 public void defineProperty(final VarScope scope,
476 final String propertyName, final Object delegateTo,
477 final Method getter, final Method setter, final int attributes) {
478
479 if ("length".equals(propertyName) && getPrototype() != null) {
480 return;
481 }
482
483 super.defineProperty(scope, propertyName, delegateTo, getter, setter, attributes);
484 }
485
486 @Override
487 public Object getArrayElement(final int index) {
488 final List<DomNode> elements = getElements();
489 if (index >= 0 && index < elements.size()) {
490 return getScriptableForElement(elements.get(index));
491 }
492 return NOT_FOUND;
493 }
494
495 @Override
496 public void setArrayElement(final int index, final Object value) {
497
498 }
499
500 @Override
501 public int getArrayLength() {
502 return getElements().size();
503 }
504 }