1 /* EventProcessingThreadImpl.java
2
3 {{IS_NOTE
4 Purpose:
5
6 Description:
7
8 History:
9 Wed Jul 20 11:24:00 2005, Created by tomyeh
10 }}IS_NOTE
11
12 Copyright (C) 2005 Potix Corporation. All Rights Reserved.
13
14 {{IS_RIGHT
15 This program is distributed under GPL Version 2.0 in the hope that
16 it will be useful, but WITHOUT ANY WARRANTY.
17 }}IS_RIGHT
18 */
19 package org.zkoss.zk.ui.impl;
20
21 import java.util.List;
22 import java.util.LinkedList;
23 import java.util.Locale;
24 import java.util.TimeZone;
25
26 import org.zkoss.lang.Threads;
27 import org.zkoss.lang.Exceptions;
28 import org.zkoss.util.Locales;
29 import org.zkoss.util.TimeZones;
30 import org.zkoss.util.logging.Log;
31
32 import org.zkoss.zk.ui.Execution;
33 import org.zkoss.zk.ui.Executions;
34 import org.zkoss.zk.ui.Desktop;
35 import org.zkoss.zk.ui.Component;
36 import org.zkoss.zk.ui.UiException;
37 import org.zkoss.zk.ui.event.Event;
38 import org.zkoss.zk.ui.util.Configuration;
39 import org.zkoss.zk.ui.sys.ExecutionCtrl;
40 import org.zkoss.zk.ui.sys.EventProcessingThread;
41
42 /** Thread to handle events.
43 * We need to handle events in a separate thread, because it might
44 * suspend (by calling {@link org.zkoss.zk.ui.sys.UiEngine#wait}), such as waiting
45 * a modal dialog to complete.
46 *
47 * @author tomyeh
48 */
49 public class EventProcessingThreadImpl extends Thread
50 implements EventProcessingThread {
51 // private static final Log log = Log.lookup(EventProcessingThreadImpl.class);
52
53 /** The processor. */
54 private EventProcessor _proc;
55 /** Part of the command: locale. */
56 private Locale _locale;
57 /** Part of the command: time zone. */
58 private TimeZone _timeZone;
59 /** Part of the result: a list of EventThreadInit instances. */
60 private List _evtThdInits;
61 /** Part of the result: a list of EventThreadCleanup instances. */
62 private List _evtThdCleanups;
63 /** Part of the result: a list of EventThreadResume instances. */
64 private List _evtThdResumes;
65 /** Part of the result. a list of EventThreadSuspend instances. */
66 private List _evtThdSuspends;
67 /** Result of the result. */
68 private Throwable _ex;
69 /** Whether the execution is activated. */
70 private boolean _acted;
71
72 private static int _nThd, _nBusyThd;
73
74 /** The mutex use to notify an event is ready for processing, or
75 * has been processed.
76 */
77 private final Object _evtmutex = new Object();
78 /** The mutex use to suspend an event processing. */
79 private Object _suspmutex;
80 /** If null, it means not ceased yet.
81 * If not null, it means it is ceased and it is a text describing the cause.
82 */
83 private String _ceased;
84 /** Whether not to show message when stopping. */
85 private boolean _silent;
86 /** Whether it is suspended. */
87 private transient boolean _suspended;
88
89 public EventProcessingThreadImpl() {
90 // if (log.debugable()) log.debug("Starting an event processing thread");
91 Threads.setDaemon(this, true);
92 start();
93 }
94
95 //EventProcessingThread//
96 public boolean isCeased() {
97 return _ceased != null;
98 }
99 public boolean isSuspended() {
100 return _suspended;
101 }
102 synchronized public boolean isIdle() {
103 return _proc == null;
104 }
105 public final Event getEvent() {
106 return _proc.getEvent();
107 }
108 public final Component getComponent() {
109 return _proc.getComponent();
110 }
111 public void sendEvent(final Component comp, Event event)
112 throws Exception {
113 // if (log.finerable()) log.finer("Process sent event: "+event);
114 if (event == null || comp == null)
115 throw new IllegalArgumentException("Both comp and event must be specified");
116 if (!(Thread.currentThread() instanceof EventProcessingThreadImpl))
117 throw new IllegalStateException("Only callable when processing an event");
118
119 final EventProcessor oldproc = _proc;
120 _proc = new EventProcessor(_proc.getDesktop(), comp, event);
121 try {
122 setup();
123 process0();
124 } finally {
125 _proc = oldproc;
126 setup();
127 }
128 }
129
130 //extra utilities//
131 /** Stops the thread. Called only by {@link org.zkoss.zk.ui.sys.UiEngine}
132 * when it is stopping.
133 * <p>Application developers shall use {@link org.zkoss.zk.ui.sys.DesktopCtrl#ceaseSuspendedThread}
134 * instead.
135 *
136 * @param cause a human readable text describing the cause.
137 * If null, an empty string is assumed.
138 */
139 public void cease(String cause) {
140 synchronized (_evtmutex) {
141 _ceased = cause != null ? cause: "";
142 _evtmutex.notifyAll();
143 }
144 if (_suspmutex != null) {
145 synchronized (_suspmutex) {
146 _suspmutex.notifyAll();
147 }
148 }
149 }
150 /** Stops the thread silently. Called by {@link org.zkoss.zk.ui.sys.UiEngine}
151 * to stop abnormally.
152 */
153 public void ceaseSilently(String cause) {
154 _silent = true;
155 cease(cause);
156 }
157
158 /** Returns the number of event threads.
159 */
160 public static final int getThreadNumber() {
161 return _nThd;
162 }
163 /** Returns the number of event threads in processing.
164 */
165 public static final int getThreadNumberInProcessing() {
166 return _nBusyThd;
167 }
168
169 /** Suspends the current thread and Waits until {@link #doResume}
170 * is called.
171 *
172 * <p>Note:
173 * <ul>
174 * <li>It is used internally only for implementing {@link org.zkoss.zk.ui.sys.UiEngine}
175 * Don't call it directly.
176 * <li>Caller must invoke {@link #newEventThreadSuspends}
177 * before calling this method. (Reason: UiEngine might have to store some info
178 * after {@link #newEventThreadSuspends} is called.
179 * <li>The current thread must be {@link EventProcessingThreadImpl}.
180 * <li>It is a static method.
181 * </ul>
182 */
183 public static void doSuspend(Object mutex) throws InterruptedException {
184 ((EventProcessingThreadImpl)Thread.currentThread()).doSuspend0(mutex);
185 }
186 private void doSuspend0(Object mutex) throws InterruptedException {
187 // if (log.finerable()) log.finer("Suspend event processing; "+_proc);
188 if (mutex == null)
189 throw new IllegalArgumentException("null mutex");
190 if (isIdle())
191 throw new InternalError("Called without processing event?");
192 if (_suspmutex != null)
193 throw new InternalError("Suspend twice?");
194
195 //Spec: locking mutex is optional for app developers
196 //so we have to lock it first
197 _suspmutex = mutex;
198 try {
199 synchronized (_suspmutex) {
200 _suspended = true;
201
202 //Bug 1814298: need to call Execution.onDeactivate
203 Execution exec = getExecution();
204 if (exec != null) {
205 ((ExecutionCtrl)exec).onDeactivate();
206 _acted = false;
207 }
208
209 //let the main thread continue
210 synchronized (_evtmutex) {
211 _evtmutex.notify();
212 }
213
214 if (_ceased == null) _suspmutex.wait();
215 }
216 } finally {
217 _suspmutex = null;
218 _suspended = false; //just in case (such as _ceased)
219 }
220
221 if (_ceased != null)
222 throw new InterruptedException(_ceased);
223
224 //being resumed
225 setup();
226 Execution exec = getExecution();
227 if (exec != null) {
228 ((ExecutionCtrl)exec).onActivate();
229 _acted = true;
230 }
231
232 final List resumes = _evtThdResumes;
233 _evtThdResumes = null;
234 if (resumes != null && !resumes.isEmpty()) {
235 _proc.getDesktop().getWebApp().getConfiguration()
236 .invokeEventThreadResumes(
237 resumes, getComponent(), getEvent());
238 //FUTURE: how to propogate errors to the client
239 }
240 }
241 private Execution getExecution() {
242 Execution exec = _proc.getDesktop().getExecution();
243 return exec != null ? exec: Executions.getCurrent();
244 //just in case that the execution is dead first
245 }
246 /** Resumes this thread and returns only if the execution (being suspended
247 * by {@link #doSuspend}) completes.
248 *
249 * <p>It executes in the main thread (i.e., the servlet thread).
250 *
251 * @return whether the event has been processed completely or just be suspended
252 */
253 public boolean doResume() throws InterruptedException {
254 if (this.equals(Thread.currentThread()))
255 throw new IllegalStateException("A thread cannot resume itself");
256 // if (log.finerable()) log.finer("Resume event processing; "+_proc);
257 if (isIdle())
258 throw new InternalError("Called without processing event?");
259 if (_suspmutex == null)
260 throw new InternalError("Resume non-suspended thread?");
261
262 //Copy first since event thread clean up them, when completed
263 final Configuration config =
264 _proc.getDesktop().getWebApp().getConfiguration();
265 final Component comp = getComponent();
266 final Event event = getEvent();
267 try {
268 _evtThdResumes = config.newEventThreadResumes(comp, event);
269
270 //Spec: locking mutex is optional for app developers
271 //so we have to lock it first
272 synchronized (_suspmutex) {
273 _suspended = false;
274 _suspmutex.notify(); //wake the suspended event thread
275 }
276
277 //wait until the event thread completes or suspends again
278 //If complete: isIdle() is true
279 //If suspend again: _suspended is true
280 synchronized (_evtmutex) {
281 if (_ceased == null && !isIdle() && !_suspended)
282 _evtmutex.wait();
283 }
284 } finally {
285 //_evtThdCleanups is null if //1) no listener;
286 //2) the event thread is suspended again (handled by another doResume)
287 invokeEventThreadCompletes(config, comp, event);
288 }
289
290 checkError();
291 return isIdle();
292 }
293
294 /** Ask this event thread to process the specified event.
295 *
296 * <p>Used internally to implement {@link org.zkoss.zk.ui.sys.UiEngine}.
297 * Application developers
298 * shall use {@link org.zkoss.zk.ui.event.Events#sendEvent} instead.
299 *
300 * @return whether the event has been processed completely or just be suspended.
301 * Recycle it only if true is returned.
302 */
303 public boolean processEvent(Desktop desktop, Component comp, Event event) {
304 if (Thread.currentThread() instanceof EventProcessingThreadImpl)
305 throw new IllegalStateException("processEvent cannot be called in an event thread");
306 if (_ceased != null)
307 throw new InternalError("The event thread has beeing stopped. Cause: "+_ceased);
308 if (_proc != null)
309 throw new InternalError("reentering processEvent not allowed");
310
311 _locale = Locales.getCurrent();
312 _timeZone = TimeZones.getCurrent();
313 _ex = null;
314
315 final EventProcessor proc = new EventProcessor(desktop, comp, event);
316 //it also check the correctness of desktop/comp/event
317 final Configuration config = desktop.getWebApp().getConfiguration();
318 _evtThdInits = config.newEventThreadInits(comp, event);
319 try {
320 synchronized (_evtmutex) {
321 _proc = proc; //Bug 1577842: don't let event thread start (and end) too early
322
323 _evtmutex.notify(); //ask the event thread to handle it
324 if (_ceased == null) {
325 _evtmutex.wait();
326 //wait until the event thread to complete or suspended
327
328 if (_suspended) {
329 config.invokeEventThreadSuspends(_evtThdSuspends, comp, event);
330 _evtThdSuspends = null;
331 }
332 }
333 }
334 } catch (InterruptedException ex) {
335 throw new UiException(ex);
336 } finally {
337 //_evtThdCleanups is null if //1) no listener;
338 //2) the event thread is suspended (then handled by doResume).
339 invokeEventThreadCompletes(config, comp, event);
340 }
341
342 checkError(); //check any error occurs
343 return isIdle();
344 }
345 /** Invokes {@link Configuration#newEventThreadSuspends}.
346 * The caller must execute in the event processing thread.
347 * It is called only for implementing {@link org.zkoss.zk.ui.sys.UiEngine}.
348 * Don't call it directly.
349 */
350 public void newEventThreadSuspends(Object mutex) {
351 if (_proc == null)
352 throw new IllegalStateException();
353
354 _evtThdSuspends = _proc.getDesktop().getWebApp().getConfiguration()
355 .newEventThreadSuspends(getComponent(), getEvent(), mutex);
356 //it might throw an exception, so process it before updating
357 //_suspended
358 }
359
360 private void invokeEventThreadCompletes(Configuration config,
361 Component comp, Event event) throws UiException {
362 if (_evtThdCleanups != null && !_evtThdCleanups.isEmpty()) {
363 final List errs = _ex != null ? null: new LinkedList();
364
365 config.invokeEventThreadCompletes(_evtThdCleanups, comp, event, errs);
366
367 if (errs != null && !errs.isEmpty())
368 throw UiException.Aide.wrap((Throwable)errs.get(0));
369 }
370 _evtThdCleanups = null;
371 }
372 /** Setup for execution. */
373 synchronized private void setup() {
374 _proc.setup();
375 }
376 /** Cleanup for execution. */
377 synchronized private void cleanup() {
378 _proc.cleanup();
379 _proc = null;
380 }
381 private void checkError() {
382 if (_ex != null) { //failed to process
383 // if (log.debugable()) log.realCause(_ex);
384 final Throwable ex = _ex;
385 _ex = null;
386 throw UiException.Aide.wrap(ex);
387 }
388 }
389
390 //-- Thread --//
391 public void run() {
392 ++_nThd;
393 try {
394 while (_ceased == null) {
395 final boolean evtAvail = !isIdle();
396 if (evtAvail) {
397 final Configuration config =
398 _proc.getDesktop().getWebApp().getConfiguration();
399 boolean cleaned = false;
400 ++_nBusyThd;
401 Execution exec = null;
402 try {
403 // if (log.finerable()) log.finer("Processing event: "+_proc);
404
405 Locales.setThreadLocal(_locale);
406 TimeZones.setThreadLocal(_timeZone);
407
408 setup();
409 exec = getExecution();
410 if (exec != null) {
411 ((ExecutionCtrl)exec).onActivate();
412 _acted = true;
413 }
414
415 final boolean b = config.invokeEventThreadInits(
416 _evtThdInits, getComponent(), getEvent());
417 _evtThdInits = null;
418
419 if (b) process0();
420 } catch (Throwable ex) {
421 cleaned = true;
422 newEventThreadCleanups(config, ex);
423 } finally {
424 --_nBusyThd;
425
426 if (!cleaned) newEventThreadCleanups(config, _ex);
427
428 // if (log.finerable()) log.finer("Real processing is done: "+_proc);
429 if (exec != null && _acted) //_acted is false if suspended is killed
430 ((ExecutionCtrl)exec).onDeactivate();
431 cleanup();
432
433 Locales.setThreadLocal(_locale = null);
434 TimeZones.setThreadLocal(_timeZone = null);
435 }
436 }
437
438 synchronized (_evtmutex) {
439 if (evtAvail)
440 _evtmutex.notify();
441 //wake the main thread OR the resuming thread
442 if (_ceased == null)
443 _evtmutex.wait();
444 //wait the main thread to issue another request
445 }
446 }
447
448 if (_silent) {
449 // if (log.debugable()) log.debug("The event processing thread stops");
450 } else {
451 System.out.println("The event processing thread stops");
452 //Don't use log because it might be stopped
453 }
454 } catch (InterruptedException ex) {
455 if (_silent) {
456 // if (log.debugable())
457 // log.debug("The event processing thread interrupted: "+Exceptions.getMessage(ex)
458 // +"\n"+Exceptions.getBriefStackTrace(ex));
459 } else {
460 System.out.println("The event processing thread interrupted: "+Exceptions.getMessage(ex));
461 //Don't use log because it might be stopped
462 }
463 } finally {
464 --_nThd;
465 }
466 }
467 /** Invokes {@link Configuration#newEventThreadCleanups}.
468 */
469 private void newEventThreadCleanups(Configuration config, Throwable ex) {
470 final List errs = new LinkedList();
471 if (ex != null) errs.add(ex);
472 _evtThdCleanups =
473 config.newEventThreadCleanups(getComponent(), getEvent(), errs);
474 _ex = errs.isEmpty() ? null: (Throwable)errs.get(0);
475 //propogate back the first exception
476 }
477
478 /** Processes the component and event.
479 */
480 private void process0() throws Exception {
481 if (_proc == null)
482 throw new IllegalStateException("Not initialized");
483 _proc.process();
484 }
485
486 //-- Object --//
487 public String toString() {
488 return "[" +getName()+": "+_proc+", ceased="+_ceased+']';
489 }
490 }