View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
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   * Container for event listener.
41   *
42   * @author Marc Guillemot
43   * @author Daniel Gredler
44   * @author Ahmed Ashour
45   * @author Frank Danek
46   * @author Ronald Brill
47   * @author Atsushi Nakagawa
48   */
49  public class EventListenersContainer implements Serializable {
50  
51      private static final Log LOG = LogFactory.getLog(EventListenersContainer.class);
52  
53      // Refactoring note: This seems ad-hoc..  Shouldn't synchronization be orchestrated between
54      // JS thread and main thread at a much higher layer?  Anyways, to preserve behaviour of prior
55      // coding where 'synchronized' was used more explicitly, we're using a ConcurrentHashMap here
56      // and using ConcurrentMap.compute() to mutate below so that mutations are atomic.  This for
57      // example avoids the case where two concurrent addListener()s can result in either being lost.
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          // This sentinel value could be some singleton instance but null
65          // isn't used for anything else so why not.
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                 // If we already have a handler then the position of the existing
105                 // placeholder should not be changed so just change the handler
106                 if (handler_ != null) {
107                     if (propertyHandler == handler_) {
108                         return this;
109                     }
110                     return withPropertyHandler(propertyHandler);
111                 }
112 
113                 // Insert the placeholder and set the handler
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         // Refactoring note: This method doesn't appear to be used
186         @Override
187         protected TypeContainer clone() {
188             return new TypeContainer(capturingListeners_, bubblingListeners_, atTargetListeners_, handler_);
189         }
190     }
191 
192     /**
193      * The constructor.
194      *
195      * @param jsNode the node.
196      */
197     public EventListenersContainer(final EventTarget jsNode) {
198         jsNode_ = jsNode;
199     }
200 
201     /**
202      * Adds an event listener.
203      *
204      * @param type the event type to listen for (like "load")
205      * @param listener the event listener
206      * @param useCapture If {@code true}, indicates that the user wishes to initiate capture (not yet implemented)
207      * @return {@code true} if the listener has been added
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      * Returns the relevant listeners.
240      *
241      * @param eventType the event type
242      * @param useCapture whether to use capture of not
243      * @return the listeners list (empty list when empty)
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      * Removes event listener.
251      *
252      * @param eventType the type
253      * @param listener the listener
254      * @param useCapture to use capture or not
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      * Sets the handler property (with a handler or something else).
267      * @param eventType the event type (like "click")
268      * @param value the new property
269      */
270     public void setEventHandler(final String eventType, final Object value) {
271         final Function handler;
272 
273         // Otherwise, ignore silently.
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         // some event don't apply on all kind of nodes, for instance "blur"
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             // no need for a copy, listeners are copy on write
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                     // Return value is only honored for property handlers (Tested in Chrome/FF/IE11)
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      * Executes bubbling listeners.
355      * @param event the event
356      * @param args arguments
357      */
358     public void executeBubblingListeners(final Event event, final Object[] args) {
359         executeEventListeners(Event.BUBBLING_PHASE, event, args);
360     }
361 
362     /**
363      * Executes capturing listeners.
364      * @param event the event
365      * @param args the arguments
366      */
367     public void executeCapturingListeners(final Event event, final Object[] args) {
368         executeEventListeners(Event.CAPTURING_PHASE, event, args);
369     }
370 
371     /**
372      * Executes listeners for events targeting the node. (non-propagation phase)
373      * @param event the event
374      * @param args the arguments
375      */
376     public void executeAtTargetListeners(final Event event, final Object[] args) {
377         executeEventListeners(Event.AT_TARGET, event, args);
378     }
379 
380     /**
381      * Returns an event handler.
382      * @param eventType the event name (e.g. "click")
383      * @return the handler function, {@code null} if the property is null or not a function
384      */
385     public Function getEventHandler(final String eventType) {
386         return getTypeContainer(eventType).handler_;
387     }
388 
389     /**
390      * Returns {@code true} if there are any event listeners for the specified event.
391      * @param eventType the event type (e.g. "click")
392      * @return {@code true} if there are any event listeners for the specified event, {@code false} otherwise
393      */
394     boolean hasEventListeners(final String eventType) {
395         return !getTypeContainer(eventType).atTargetListeners_.isEmpty();
396     }
397 
398     /**
399      * {@inheritDoc}
400      */
401     @Override
402     public String toString() {
403         return getClass().getSimpleName() + "[node=" + jsNode_ + " handlers=" + typeContainers_.keySet() + "]";
404     }
405 }