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 }