1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.htmlunit.javascript.host.event;
16
17 import java.io.Serializable;
18 import java.util.ArrayList;
19 import java.util.Collections;
20 import java.util.List;
21 import java.util.Locale;
22 import java.util.concurrent.ConcurrentHashMap;
23 import java.util.concurrent.ConcurrentMap;
24
25 import org.apache.commons.logging.Log;
26 import org.apache.commons.logging.LogFactory;
27 import org.htmlunit.ScriptResult;
28 import org.htmlunit.corejs.javascript.Function;
29 import org.htmlunit.corejs.javascript.NativeObject;
30 import org.htmlunit.corejs.javascript.Scriptable;
31 import org.htmlunit.corejs.javascript.ScriptableObject;
32 import org.htmlunit.html.DomNode;
33 import org.htmlunit.html.HtmlPage;
34 import org.htmlunit.javascript.JavaScriptEngine;
35 import org.htmlunit.javascript.host.Window;
36 import org.htmlunit.javascript.host.html.HTMLDocument;
37 import org.htmlunit.javascript.host.html.HTMLElement;
38
39
40
41
42
43
44
45
46
47
48
49 public class EventListenersContainer implements Serializable {
50
51 private static final Log LOG = LogFactory.getLog(EventListenersContainer.class);
52
53
54
55
56
57
58 private final ConcurrentMap<String, TypeContainer> typeContainers_ = new ConcurrentHashMap<>();
59 private final EventTarget jsNode_;
60
61 private static class TypeContainer implements Serializable {
62 public static final TypeContainer EMPTY = new TypeContainer();
63
64
65
66 private static final Scriptable EVENT_HANDLER_PLACEHOLDER = null;
67
68 private final List<Scriptable> capturingListeners_;
69 private final List<Scriptable> bubblingListeners_;
70 private final List<Scriptable> atTargetListeners_;
71 private final Function handler_;
72
73 TypeContainer() {
74 capturingListeners_ = Collections.emptyList();
75 bubblingListeners_ = Collections.emptyList();
76 atTargetListeners_ = Collections.emptyList();
77 handler_ = null;
78 }
79
80 private TypeContainer(final List<Scriptable> capturingListeners,
81 final List<Scriptable> bubblingListeners, final List<Scriptable> atTargetListeners,
82 final Function handler) {
83 capturingListeners_ = capturingListeners;
84 bubblingListeners_ = bubblingListeners;
85 atTargetListeners_ = atTargetListeners;
86 handler_ = handler;
87 }
88
89 List<Scriptable> getListeners(final int eventPhase) {
90 switch (eventPhase) {
91 case Event.CAPTURING_PHASE:
92 return capturingListeners_;
93 case Event.AT_TARGET:
94 return atTargetListeners_;
95 case Event.BUBBLING_PHASE:
96 return bubblingListeners_;
97 default:
98 throw new UnsupportedOperationException("eventPhase: " + eventPhase);
99 }
100 }
101
102 public TypeContainer setPropertyHandler(final Function propertyHandler) {
103 if (propertyHandler != null) {
104
105
106 if (handler_ != null) {
107 if (propertyHandler == handler_) {
108 return this;
109 }
110 return withPropertyHandler(propertyHandler);
111 }
112
113
114 return withPropertyHandler(propertyHandler).addListener(EVENT_HANDLER_PLACEHOLDER, false);
115 }
116 if (handler_ == null) {
117 return this;
118 }
119 return removeListener(EVENT_HANDLER_PLACEHOLDER, false).withPropertyHandler(null);
120 }
121
122 private TypeContainer withPropertyHandler(final Function propertyHandler) {
123 return new TypeContainer(capturingListeners_, bubblingListeners_, atTargetListeners_, propertyHandler);
124 }
125
126 public TypeContainer addListener(final Scriptable listener, final boolean useCapture) {
127
128 List<Scriptable> capturingListeners = capturingListeners_;
129 List<Scriptable> bubblingListeners = bubblingListeners_;
130 final List<Scriptable> listeners = useCapture ? capturingListeners : bubblingListeners;
131
132 if (listeners.contains(listener)) {
133 return this;
134 }
135
136 List<Scriptable> newListeners = new ArrayList<>(listeners.size() + 1);
137 newListeners.addAll(listeners);
138 newListeners.add(listener);
139 newListeners = Collections.unmodifiableList(newListeners);
140
141 if (useCapture) {
142 capturingListeners = newListeners;
143 }
144 else {
145 bubblingListeners = newListeners;
146 }
147
148 List<Scriptable> atTargetListeners = new ArrayList<>(atTargetListeners_.size() + 1);
149 atTargetListeners.addAll(atTargetListeners_);
150 atTargetListeners.add(listener);
151 atTargetListeners = Collections.unmodifiableList(atTargetListeners);
152
153 return new TypeContainer(capturingListeners, bubblingListeners, atTargetListeners, handler_);
154 }
155
156 public TypeContainer removeListener(final Scriptable listener, final boolean useCapture) {
157
158 List<Scriptable> capturingListeners = capturingListeners_;
159 List<Scriptable> bubblingListeners = bubblingListeners_;
160 final List<Scriptable> listeners = useCapture ? capturingListeners : bubblingListeners;
161
162 final int idx = listeners.indexOf(listener);
163 if (idx < 0) {
164 return this;
165 }
166
167 List<Scriptable> newListeners = new ArrayList<>(listeners);
168 newListeners.remove(idx);
169 newListeners = Collections.unmodifiableList(newListeners);
170
171 if (useCapture) {
172 capturingListeners = newListeners;
173 }
174 else {
175 bubblingListeners = newListeners;
176 }
177
178 List<Scriptable> atTargetListeners = new ArrayList<>(atTargetListeners_);
179 atTargetListeners.remove(listener);
180 atTargetListeners = Collections.unmodifiableList(atTargetListeners);
181
182 return new TypeContainer(capturingListeners, bubblingListeners, atTargetListeners, handler_);
183 }
184
185
186 @Override
187 protected TypeContainer clone() {
188 return new TypeContainer(capturingListeners_, bubblingListeners_, atTargetListeners_, handler_);
189 }
190 }
191
192
193
194
195
196
197 public EventListenersContainer(final EventTarget jsNode) {
198 jsNode_ = jsNode;
199 }
200
201
202
203
204
205
206
207
208
209 public boolean addEventListener(final String type, final Scriptable listener, final boolean useCapture) {
210 if (null == listener) {
211 return true;
212 }
213
214 final boolean[] added = {false};
215 typeContainers_.compute(type.toLowerCase(Locale.ROOT), (k, container) -> {
216 if (container == null) {
217 container = TypeContainer.EMPTY;
218 }
219 final TypeContainer newContainer = container.addListener(listener, useCapture);
220 added[0] = newContainer != container;
221 return newContainer;
222 });
223
224 if (!added[0]) {
225 if (LOG.isDebugEnabled()) {
226 LOG.debug(type + " listener already registered, skipping it (" + listener + ")");
227 }
228 return false;
229 }
230 return true;
231 }
232
233 private TypeContainer getTypeContainer(final String type) {
234 final String typeLC = type.toLowerCase(Locale.ROOT);
235 return typeContainers_.getOrDefault(typeLC, TypeContainer.EMPTY);
236 }
237
238
239
240
241
242
243
244
245 public List<Scriptable> getListeners(final String eventType, final boolean useCapture) {
246 return getTypeContainer(eventType).getListeners(useCapture ? Event.CAPTURING_PHASE : Event.BUBBLING_PHASE);
247 }
248
249
250
251
252
253
254
255
256 void removeEventListener(final String eventType, final Scriptable listener, final boolean useCapture) {
257 if (listener == null) {
258 return;
259 }
260
261 typeContainers_.computeIfPresent(eventType.toLowerCase(Locale.ROOT),
262 (k, container) -> container.removeListener(listener, useCapture));
263 }
264
265
266
267
268
269
270 public void setEventHandler(final String eventType, final Object value) {
271 final Function handler;
272
273
274 if (JavaScriptEngine.isUndefined(value) || !(value instanceof Function)) {
275 handler = null;
276 }
277 else {
278 handler = (Function) value;
279 }
280
281 typeContainers_.compute(eventType.toLowerCase(Locale.ROOT), (k, container) -> {
282 if (container == null) {
283 container = TypeContainer.EMPTY;
284 }
285 return container.setPropertyHandler(handler);
286 });
287 }
288
289 private void executeEventListeners(final int eventPhase, final Event event, final Object[] args) {
290 final DomNode node = jsNode_.getDomNodeOrNull();
291
292 if (node != null && !node.handles(event)) {
293 return;
294 }
295
296 final TypeContainer container = getTypeContainer(event.getType());
297 final List<Scriptable> listeners = container.getListeners(eventPhase);
298 if (!listeners.isEmpty()) {
299 event.setCurrentTarget(jsNode_);
300
301 final HtmlPage page;
302 if (jsNode_ instanceof Window) {
303 page = (HtmlPage) jsNode_.getDomNodeOrDie();
304 }
305 else {
306 final Scriptable parentScope = jsNode_.getParentScope();
307 if (parentScope instanceof Window) {
308 page = (HtmlPage) ((Window) parentScope).getDomNodeOrDie();
309 }
310 else if (parentScope instanceof HTMLDocument) {
311 page = ((HTMLDocument) parentScope).getPage();
312 }
313 else {
314 page = ((HTMLElement) parentScope).getDomNodeOrDie().getHtmlPageOrNull();
315 }
316 }
317
318
319 for (Scriptable listener : listeners) {
320 boolean isPropertyHandler = false;
321 if (listener == TypeContainer.EVENT_HANDLER_PLACEHOLDER) {
322 listener = container.handler_;
323 isPropertyHandler = true;
324 }
325 Function function = null;
326 Scriptable thisObject = null;
327 if (listener instanceof Function) {
328 function = (Function) listener;
329 thisObject = jsNode_;
330 }
331 else if (listener instanceof NativeObject) {
332 final Object handleEvent = ScriptableObject.getProperty(listener, "handleEvent");
333 if (handleEvent instanceof Function) {
334 function = (Function) handleEvent;
335 thisObject = listener;
336 }
337 }
338 if (function != null) {
339 final ScriptResult result =
340 page.executeJavaScriptFunction(function, thisObject, args, node);
341
342 if (isPropertyHandler && !ScriptResult.isUndefined(result)) {
343 event.handlePropertyHandlerReturnValue(result.getJavaScriptResult());
344 }
345 }
346 if (event.isImmediatePropagationStopped()) {
347 return;
348 }
349 }
350 }
351 }
352
353
354
355
356
357
358 public void executeBubblingListeners(final Event event, final Object[] args) {
359 executeEventListeners(Event.BUBBLING_PHASE, event, args);
360 }
361
362
363
364
365
366
367 public void executeCapturingListeners(final Event event, final Object[] args) {
368 executeEventListeners(Event.CAPTURING_PHASE, event, args);
369 }
370
371
372
373
374
375
376 public void executeAtTargetListeners(final Event event, final Object[] args) {
377 executeEventListeners(Event.AT_TARGET, event, args);
378 }
379
380
381
382
383
384
385 public Function getEventHandler(final String eventType) {
386 return getTypeContainer(eventType).handler_;
387 }
388
389
390
391
392
393
394 boolean hasEventListeners(final String eventType) {
395 return !getTypeContainer(eventType).atTargetListeners_.isEmpty();
396 }
397
398
399
400
401 @Override
402 public String toString() {
403 return getClass().getSimpleName() + "[node=" + jsNode_ + " handlers=" + typeContainers_.keySet() + "]";
404 }
405 }