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 import org.htmlunit.javascript.configuration.JsxClass;
38
39
40
41
42
43
44
45
46
47
48
49 @JsxClass(isJSObject = false)
50 public class AbstractList extends HtmlUnitScriptable implements ExternalArrayData {
51
52
53
54
55 public enum EffectOnCache {
56
57 NONE,
58
59 RESET
60 }
61
62 private boolean avoidObjectDetection_;
63
64 private boolean attributeChangeSensitive_;
65
66
67
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
96
97 public AbstractList() {
98 super();
99 }
100
101
102
103
104
105
106
107
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
130
131
132 @Override
133 public boolean avoidObjectDetection() {
134 return avoidObjectDetection_;
135 }
136
137
138
139
140 public void setAvoidObjectDetection(final boolean newValue) {
141 avoidObjectDetection_ = newValue;
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 protected Supplier<List<DomNode>> getElementSupplier() {
159 return elementsSupplier_;
160 }
161
162
163
164
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
175
176 protected Predicate<DomNode> getIsMatchingPredicate() {
177 return isMatchingPredicate_;
178 }
179
180
181
182
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
193
194
195
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
220
221
222 public List<DomNode> getElements() {
223
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
238
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
263
264
265
266
267
268
269 @Override
270 protected Object getWithPreemption(final String name) {
271
272
273 if ("length".equals(name)) {
274 return NOT_FOUND;
275 }
276
277 final List<DomNode> elements = getElements();
278
279
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
301 return getWithPreemptionByName(name, elements);
302 }
303
304
305
306
307
308
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
316
317
318
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
339 final DomNode domNode = getDomNodeOrNull();
340 final AbstractList collection = create(domNode, matchingElements);
341 collection.setAvoidObjectDetection(true);
342 return collection;
343 }
344
345
346
347
348
349 public int getLength() {
350 return getElements().size();
351 }
352
353
354
355
356 @Override
357 public String toString() {
358 return getClass().getSimpleName() + " for " + getDomNodeOrNull();
359 }
360
361
362
363
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
398
399 @Override
400 public void nodeAdded(final DomChangeEvent event) {
401 clearCache();
402 }
403
404
405
406
407 @Override
408 public void nodeDeleted(final DomChangeEvent event) {
409 clearCache();
410 }
411
412
413
414
415 @Override
416 public void attributeAdded(final HtmlAttributeChangeEvent event) {
417 handleChangeOnCache(event);
418 }
419
420
421
422
423 @Override
424 public void attributeRemoved(final HtmlAttributeChangeEvent event) {
425 handleChangeOnCache(event);
426 }
427
428
429
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
467
468
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
479
480 @Override
481 public void defineProperty(final String propertyName, final Object delegateTo,
482 final Method getter, final Method setter, final int attributes) {
483
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
503 }
504
505 @Override
506 public int getArrayLength() {
507 return getElements().size();
508 }
509 }