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
137
138 public void setAvoidObjectDetection(final boolean newValue) {
139 avoidObjectDetection_ = newValue;
140 }
141
142
143
144
145
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
157
158
159
160 protected Supplier<List<DomNode>> getElementSupplier() {
161 return elementsSupplier_;
162 }
163
164
165
166
167
168 public void setElementsSupplier(final Supplier<List<DomNode>> elementsSupplier) {
169 if (elementsSupplier == null) {
170 throw new NullPointerException("ElementsSupplier can't be null");
171 }
172 elementsSupplier_ = elementsSupplier;
173 }
174
175
176
177
178
179
180 protected Predicate<DomNode> getIsMatchingPredicate() {
181 return isMatchingPredicate_;
182 }
183
184
185
186
187
188 public void setIsMatchingPredicate(final Predicate<DomNode> isMatchingPredicate) {
189 if (isMatchingPredicate == null) {
190 throw new NullPointerException("IsMatchingPredicate can't be null");
191 }
192 isMatchingPredicate_ = isMatchingPredicate;
193 }
194
195
196
197
198
199
200
201 protected Object getIt(final Object o) {
202 if (o instanceof Number n) {
203 final int i = n.intValue();
204 return get(i, this);
205 }
206 final String key = String.valueOf(o);
207 return get(key, this);
208 }
209
210 @Override
211 public void setDomNode(final DomNode domNode, final boolean assignScriptObject) {
212 final DomNode oldDomNode = getDomNodeOrNull();
213
214 super.setDomNode(domNode, assignScriptObject);
215
216 if (oldDomNode != domNode) {
217 listenerRegistered_ = false;
218 }
219 }
220
221
222
223
224
225 public List<DomNode> getElements() {
226
227 List<DomNode> cachedElements = cachedElements_;
228
229 if (cachedElements == null) {
230 if (getParentScope() == null) {
231 cachedElements = new ArrayList<>();
232 }
233 else {
234 cachedElements = elementsSupplier_.get();
235 }
236 cachedElements_ = cachedElements;
237 }
238 registerListener();
239
240
241
242 return cachedElements;
243 }
244
245
246
247
248 private void registerListener() {
249 if (!listenerRegistered_) {
250 final DomNode domNode = getDomNodeOrNull();
251 if (domNode != null) {
252 final DomHtmlAttributeChangeListenerImpl listener = new DomHtmlAttributeChangeListenerImpl(this);
253 domNode.addDomChangeListener(listener);
254 if (attributeChangeSensitive_) {
255 if (domNode instanceof HtmlElement element) {
256 element.addHtmlAttributeChangeListener(listener);
257 }
258 else if (domNode instanceof HtmlPage page) {
259 page.addHtmlAttributeChangeListener(listener);
260 }
261 }
262 listenerRegistered_ = true;
263 }
264 }
265 }
266
267
268
269
270
271
272
273
274
275 @Override
276 protected Object getWithPreemption(final String name) {
277
278
279 if ("length".equals(name)) {
280 return NOT_FOUND;
281 }
282
283 final List<DomNode> elements = getElements();
284
285
286 final List<DomNode> matchingElements = new ArrayList<>();
287
288 for (final DomNode next : elements) {
289 if (next instanceof DomElement element) {
290 final String id = element.getId();
291 if (name.equals(id)) {
292 matchingElements.add(next);
293 }
294 }
295 }
296
297 if (matchingElements.size() == 1) {
298 return getScriptableForElement(matchingElements.get(0));
299 }
300 else if (!matchingElements.isEmpty()) {
301 final AbstractList collection = create(getDomNodeOrDie(), matchingElements);
302 collection.setAvoidObjectDetection(true);
303 return collection;
304 }
305
306
307 return getWithPreemptionByName(name, elements);
308 }
309
310
311
312
313
314
315
316
317 protected AbstractList create(final DomNode parentScope, final List<DomNode> initialElements) {
318 throw new IllegalAccessError("Creation of AbstractListInstances is not allowed.");
319 }
320
321
322
323
324
325
326
327 protected Object getWithPreemptionByName(final String name, final List<DomNode> elements) {
328 final List<DomNode> matchingElements = new ArrayList<>();
329 for (final DomNode next : elements) {
330 if (next instanceof DomElement element) {
331 final String nodeName = element.getAttributeDirect(DomElement.NAME_ATTRIBUTE);
332 if (name.equals(nodeName)) {
333 matchingElements.add(next);
334 }
335 }
336 }
337
338 if (matchingElements.isEmpty()) {
339 return NOT_FOUND;
340 }
341 else if (matchingElements.size() == 1) {
342 return getScriptableForElement(matchingElements.get(0));
343 }
344
345
346 final DomNode domNode = getDomNodeOrNull();
347 final AbstractList collection = create(domNode, matchingElements);
348 collection.setAvoidObjectDetection(true);
349 return collection;
350 }
351
352
353
354
355
356
357 public int getLength() {
358 return getElements().size();
359 }
360
361
362
363
364 @Override
365 public String toString() {
366 return getClass().getSimpleName() + " for " + getDomNodeOrNull();
367 }
368
369
370
371
372
373 @Override
374 protected Object equivalentValues(final Object other) {
375 if (other == this) {
376 return Boolean.TRUE;
377 }
378 else if (other instanceof AbstractList otherArray) {
379 final DomNode domNode = getDomNodeOrNull();
380 final DomNode domNodeOther = otherArray.getDomNodeOrNull();
381 if (getClass() == other.getClass()
382 && domNode == domNodeOther
383 && getElements().equals(otherArray.getElements())) {
384 return Boolean.TRUE;
385 }
386 return NOT_FOUND;
387 }
388
389 return super.equivalentValues(other);
390 }
391
392 private static final class DomHtmlAttributeChangeListenerImpl
393 implements DomChangeListener, HtmlAttributeChangeListener {
394
395 private final transient WeakReference<AbstractList> nodeList_;
396
397 DomHtmlAttributeChangeListenerImpl(final AbstractList nodeList) {
398 super();
399
400 nodeList_ = new WeakReference<>(nodeList);
401 }
402
403
404
405
406 @Override
407 public void nodeAdded(final DomChangeEvent event) {
408 clearCache();
409 }
410
411
412
413
414 @Override
415 public void nodeDeleted(final DomChangeEvent event) {
416 clearCache();
417 }
418
419
420
421
422 @Override
423 public void attributeAdded(final HtmlAttributeChangeEvent event) {
424 handleChangeOnCache(event);
425 }
426
427
428
429
430 @Override
431 public void attributeRemoved(final HtmlAttributeChangeEvent event) {
432 handleChangeOnCache(event);
433 }
434
435
436
437
438 @Override
439 public void attributeReplaced(final HtmlAttributeChangeEvent event) {
440 final AbstractList nodes = nodeList_.get();
441 if (null == nodes) {
442 return;
443 }
444 if (nodes.attributeChangeSensitive_) {
445 handleChangeOnCache(event);
446 }
447 }
448
449
450
451
452
453
454 private void handleChangeOnCache(final HtmlAttributeChangeEvent event) {
455 final AbstractList nodes = nodeList_.get();
456 if (null == nodes) {
457 return;
458 }
459
460 final EffectOnCache effectOnCache = nodes.effectOnCacheFunction_.apply(event);
461 if (EffectOnCache.NONE == effectOnCache) {
462 return;
463 }
464 if (EffectOnCache.RESET == effectOnCache) {
465 clearCache();
466 }
467 }
468
469 private void clearCache() {
470 final AbstractList nodes = nodeList_.get();
471 if (null != nodes) {
472 nodes.cachedElements_ = null;
473 }
474 }
475 }
476
477
478
479
480
481
482
483 protected Scriptable getScriptableForElement(final Object object) {
484 if (object instanceof Scriptable scriptable) {
485 return scriptable;
486 }
487 return getScriptableFor(object);
488 }
489
490
491
492
493 @Override
494 public void defineProperty(final VarScope scope,
495 final String propertyName, final Object delegateTo,
496 final Method getter, final Method setter, final int attributes) {
497
498 if ("length".equals(propertyName) && getPrototype() != null) {
499 return;
500 }
501
502 super.defineProperty(scope, propertyName, delegateTo, getter, setter, attributes);
503 }
504
505 @Override
506 public Object getArrayElement(final int index) {
507 final List<DomNode> elements = getElements();
508 if (index >= 0 && index < elements.size()) {
509 return getScriptableForElement(elements.get(index));
510 }
511 return NOT_FOUND;
512 }
513
514 @Override
515 public void setArrayElement(final int index, final Object value) {
516
517 }
518
519 @Override
520 public int getArrayLength() {
521 return getElements().size();
522 }
523 }