/* EventProcessingThreadImpl.java Purpose: Description: History: Wed Jul 20 11:24:00 2005, Created by tomyeh Copyright (C) 2005 Potix Corporation. All Rights Reserved. {{IS_RIGHT This program is distributed under LGPL Version 2.1 in the hope that it will be useful, but WITHOUT ANY WARRANTY. }}IS_RIGHT */ package org.zkoss.zk.ui.impl; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.TimeZone; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.zkoss.lang.Exceptions; import org.zkoss.lang.Threads; import org.zkoss.util.Locales; import org.zkoss.util.TimeZones; import org.zkoss.zk.ui.Component; import org.zkoss.zk.ui.Desktop; import org.zkoss.zk.ui.Execution; import org.zkoss.zk.ui.Executions; import org.zkoss.zk.ui.UiException; import org.zkoss.zk.ui.event.Event; import org.zkoss.zk.ui.event.EventThreadCleanup; import org.zkoss.zk.ui.event.EventThreadInit; import org.zkoss.zk.ui.event.EventThreadResume; import org.zkoss.zk.ui.event.EventThreadSuspend; import org.zkoss.zk.ui.sys.EventProcessingThread; import org.zkoss.zk.ui.sys.ExecutionCtrl; import org.zkoss.zk.ui.util.Configuration; import org.zkoss.zk.ui.util.ExecutionMonitor; /** Thread to handle events. * We need to handle events in a separate thread, because it might * suspend (by calling {@link org.zkoss.zk.ui.sys.UiEngine#wait}), such as waiting * a modal dialog to complete. * * @author tomyeh */ public class EventProcessingThreadImpl extends Thread implements EventProcessingThread { private static final Logger log = LoggerFactory.getLogger(EventProcessingThreadImpl.class); /** The processor. */ private EventProcessor _proc; /** Part of the command: locale. */ private Locale _locale; /** Part of the command: time zone. */ private TimeZone _timeZone; /** Part of the result: a list of EventThreadInit instances. */ private List<EventThreadInit> _evtThdInits; /** Part of the result: a list of EventThreadCleanup instances. */ private List<EventThreadCleanup> _evtThdCleanups; /** Part of the result: a list of EventThreadResume instances. */ private List<EventThreadResume> _evtThdResumes; /** Part of the result. a list of EventThreadSuspend instances. */ private List<EventThreadSuspend> _evtThdSuspends; /** Result of the result. */ private Throwable _ex; /** Whether the execution is activated. */ private boolean _acted; private static int _nThd, _nBusyThd; /** The mutex use to notify an event is ready for processing, or * has been processed. */ private final Object _evtmutex = new Object(); /** The mutex use to suspend an event processing. */ private Object _suspmutex; /** If null, it means not ceased yet. * If not null, it means it is ceased and it is a text describing the cause. */ private String _ceased; /** Whether not to show message when stopping. */ private boolean _silent; /** Whether it is suspended. */ private boolean _suspended; public EventProcessingThreadImpl() { // if (log.isDebugEnabled()) log.debug("Starting an event processing thread"); Threads.setDaemon(this, true); start(); } //EventProcessingThread// public boolean isCeased() { return _ceased != null || !isAlive(); } public boolean isSuspended() { return _suspended; } public synchronized boolean isIdle() { return _proc == null; } public final Event getEvent() { return _proc.getEvent(); } public final Component getComponent() { return _proc.getComponent(); } public void sendEvent(final Component comp, Event event) throws Exception { // if (log.finerable()) log.finer("Process sent event: "+event); if (event == null || comp == null) throw new IllegalArgumentException("Both comp and event must be specified"); if (!(Thread.currentThread() instanceof EventProcessingThreadImpl)) throw new IllegalStateException("Only callable when processing an event"); final EventProcessor oldproc = _proc; _proc = new EventProcessor(_proc.getDesktop(), comp, event); try { setup(); process0(); } finally { _proc = oldproc; if (_ceased != null) throw new InterruptedException(_ceased); //Bug 2819521: cease() resumes suspend threads, which shall stop setup(); } } //extra utilities// /** Stops the thread. Called only by {@link org.zkoss.zk.ui.sys.UiEngine} * when it is stopping. * <p>Application developers shall use {@link org.zkoss.zk.ui.sys.DesktopCtrl#ceaseSuspendedThread} * instead. * * @param cause a human readable text describing the cause. * If null, an empty string is assumed. */ public void cease(String cause) { synchronized (_evtmutex) { _ceased = cause != null ? cause : ""; _evtmutex.notifyAll(); } if (_suspmutex != null) { synchronized (_suspmutex) { _suspmutex.notifyAll(); } } } /** Stops the thread silently. Called by {@link org.zkoss.zk.ui.sys.UiEngine} * to stop abnormally. */ public void ceaseSilently(String cause) { _silent = true; cease(cause); } /** Returns the number of event threads. */ public static final int getThreadNumber() { return _nThd; } /** Returns the number of event threads in processing. */ public static final int getThreadNumberInProcessing() { return _nBusyThd; } /** Suspends the current thread and Waits until {@link #doResume} * is called. * * <p>Note: * <ul> * <li>It is used internally only for implementing {@link org.zkoss.zk.ui.sys.UiEngine} * Don't call it directly. * <li>Caller must invoke {@link #newEventThreadSuspends} * before calling this method. (Reason: UiEngine might have to store some info * after {@link #newEventThreadSuspends} is called. * <li>The current thread must be {@link EventProcessingThreadImpl}. * <li>It is a static method. * </ul> */ public static void doSuspend(Object mutex) throws InterruptedException { ((EventProcessingThreadImpl) Thread.currentThread()).doSuspend0(mutex); } private void doSuspend0(Object mutex) throws InterruptedException { // if (log.finerable()) log.finer("Suspend event processing; "+_proc); if (mutex == null) throw new IllegalArgumentException("null mutex"); if (isIdle()) throw new InternalError("Called without processing event?"); if (_suspmutex != null) throw new InternalError("Suspend twice?"); //Spec: locking mutex is optional for app developers //so we have to lock it first ExecutionMonitor execmon = null; _suspmutex = mutex; try { synchronized (_suspmutex) { _suspended = true; //Bug 1814298: need to call Execution.onDeactivate Execution exec = getExecution(); if (exec != null) { _acted = false; try { ((ExecutionCtrl) exec).onDeactivate(); } catch (Throwable ex) { log.warn("Ignored deactivate failure", ex); } } //let the main thread continue synchronized (_evtmutex) { _evtmutex.notify(); } if (_ceased == null) { execmon = _proc.getDesktop().getWebApp().getConfiguration().getExecutionMonitor(); //init only required, so eventResume called if-only-if eventSuspend called if (execmon != null) execmon.eventSuspend(getEvent()); _suspmutex.wait(); } } } finally { _suspmutex = null; _suspended = false; //just in case (such as _ceased) if (execmon != null) execmon.eventResume(getEvent()); } if (_ceased != null) throw new InterruptedException(_ceased); //being resumed setup(); Execution exec = getExecution(); if (exec != null) { ((ExecutionCtrl) exec).onActivate(); _acted = true; } final List<EventThreadResume> resumes = _evtThdResumes; _evtThdResumes = null; if (resumes != null && !resumes.isEmpty()) { _proc.getDesktop().getWebApp().getConfiguration().invokeEventThreadResumes(resumes, getComponent(), getEvent()); //FUTURE: how to propagate errors to the client } } private Execution getExecution() { Execution exec = _proc.getDesktop().getExecution(); return exec != null ? exec : Executions.getCurrent(); //just in case that the execution is dead first } /** Resumes this thread and returns only if the execution (being suspended * by {@link #doSuspend}) completes. * * <p>It executes in the main thread (i.e., the servlet thread). * * @return whether the event has been processed completely or just be suspended */ public boolean doResume() throws InterruptedException { if (this.equals(Thread.currentThread())) throw new IllegalStateException("A thread cannot resume itself"); // if (log.finerable()) log.finer("Resume event processing; "+_proc); if (isIdle()) throw new InternalError("Called without processing event?"); if (_suspmutex == null) throw new InternalError("Resume non-suspended thread?"); //Copy first since event thread clean up them, when completed final Configuration config = _proc.getDesktop().getWebApp().getConfiguration(); final Component comp = getComponent(); final Event event = getEvent(); try { _evtThdResumes = config.newEventThreadResumes(comp, event); //Spec: locking mutex is optional for app developers //so we have to lock it first synchronized (_suspmutex) { _suspended = false; _suspmutex.notify(); //wake the suspended event thread } //wait until the event thread completes or suspends again //If complete: isIdle() is true //If suspend again: _suspended is true synchronized (_evtmutex) { if (_ceased == null && !isIdle() && !_suspended) _evtmutex.wait(); } } finally { //_evtThdCleanups is null if //1) no listener; //2) the event thread is suspended again (handled by another doResume) invokeEventThreadCompletes(config, comp, event); } checkError(); return isIdle(); } /** Ask this event thread to process the specified event. * * <p>Used internally to implement {@link org.zkoss.zk.ui.sys.UiEngine}. * Application developers * shall use {@link org.zkoss.zk.ui.event.Events#sendEvent} instead. * * @return whether the event has been processed completely or just be suspended. * Recycle it only if true is returned. */ public boolean processEvent(Desktop desktop, Component comp, Event event) { if (Thread.currentThread() instanceof EventProcessingThreadImpl) throw new IllegalStateException("processEvent cannot be called in an event thread"); if (_ceased != null) throw new InternalError("The event thread has beeing stopped. Cause: " + _ceased); if (_proc != null) throw new InternalError("reentering processEvent not allowed"); _locale = Locales.getCurrent(); _timeZone = TimeZones.getCurrent(); _ex = null; final EventProcessor proc = new EventProcessor(desktop, comp, event); //it also check the correctness of desktop/comp/event final Configuration config = desktop.getWebApp().getConfiguration(); _evtThdInits = config.newEventThreadInits(comp, event); try { long evtTimeWarn = config.getEventTimeWarning(); long begt = 0; if (evtTimeWarn > 0) { begt = System.currentTimeMillis(); evtTimeWarn *= 1000; } for (;;) { synchronized (_evtmutex) { _proc = proc; //Bug 1577842: don't let event thread start (and end) too early _evtmutex.notify(); //ask the event thread to handle it if (_ceased == null) { if (evtTimeWarn > 0) _evtmutex.wait(evtTimeWarn); else _evtmutex.wait(); //wait until the event thread to complete or suspended if (_suspended) { config.invokeEventThreadSuspends(_evtThdSuspends, comp, event); _evtThdSuspends = null; break; } if (_proc == null || _ceased != null) break; if (!isAlive()) throw new UiException("The event processing thread was aborted"); log.warn("The event processing takes more than " + ((System.currentTimeMillis() - begt) / 1000) + " seconds: " + proc); } } } } catch (InterruptedException ex) { throw new UiException(ex); } finally { //Note: newEventThreadCleanups was called in run(), i.e., //in the event thread //_evtThdCleanups is null if //1) no listener; //2) the event thread is suspended (then handled by doResume). invokeEventThreadCompletes(config, comp, event); } checkError(); //check any error occurs return isIdle(); } /** Invokes {@link Configuration#newEventThreadSuspends}. * The caller must execute in the event processing thread. * It is called only for implementing {@link org.zkoss.zk.ui.sys.UiEngine}. * Don't call it directly. */ public void newEventThreadSuspends(Object mutex) { if (_proc == null) throw new IllegalStateException(); _evtThdSuspends = _proc.getDesktop().getWebApp().getConfiguration().newEventThreadSuspends(getComponent(), getEvent(), mutex); //it might throw an exception, so process it before updating //_suspended } private void invokeEventThreadCompletes(Configuration config, Component comp, Event event) throws UiException { final List<Throwable> errs = new LinkedList<Throwable>(); if (_ex != null) errs.add(_ex); if (_evtThdCleanups != null && !_evtThdCleanups.isEmpty()) config.invokeEventThreadCompletes(_evtThdCleanups, comp, event, errs, _ceased != null); _evtThdCleanups = null; _ex = errs.isEmpty() ? null : errs.get(0); } /** Setup for execution. */ private synchronized void setup() { _proc.setup(); } /** Cleanup for execution. */ private synchronized void cleanup() { _proc.cleanup(); _proc = null; } private void checkError() { if (_ex != null) { //failed to process // if (log.isDebugEnabled()) log.realCause(_ex); final Throwable ex = _ex; _ex = null; throw UiException.Aide.wrap(ex); } } //-- Thread --// public void run() { ++_nThd; try { while (_ceased == null) { if (!isIdle()) { final Configuration config = _proc.getDesktop().getWebApp().getConfiguration(); boolean cleaned = false; ++_nBusyThd; Execution exec = null; try { // if (log.finerable()) log.finer("Processing event: "+_proc); Locales.setThreadLocal(_locale); TimeZones.setThreadLocal(_timeZone); setup(); exec = getExecution(); if (exec != null) { ((ExecutionCtrl) exec).onActivate(); _acted = true; } final boolean b = config.invokeEventThreadInits(_evtThdInits, getComponent(), getEvent()); _evtThdInits = null; if (b) process0(); } catch (Throwable ex) { cleaned = true; newEventThreadCleanups(config, ex); //ex will be assigned to _ex if newEventThreadCleanups not 'eat' it } finally { --_nBusyThd; if (!cleaned) newEventThreadCleanups(config, _ex); // if (log.finerable()) log.finer("Real processing is done: "+_proc); if (exec != null && _acted) { //_acted is false if suspended is killed _acted = false; try { ((ExecutionCtrl) exec).onDeactivate(); } catch (Throwable ex) { log.warn("Ignored deactivate failure", ex); } } cleanup(); Locales.setThreadLocal(_locale = null); TimeZones.setThreadLocal(_timeZone = null); if (_ex != null && _ceased != null) _ex = null; //avoid annoying message (Bug 2819521) } } synchronized (_evtmutex) { _evtmutex.notify(); //wake the main thread OR the resuming thread if (_ceased == null) _evtmutex.wait(); //wait the main thread to issue another request } } // System.out.println(Thread.currentThread()+" stopped: "+_ceased); } catch (Throwable ex) { if (_ceased == null) _ceased = Exceptions.getMessage(ex); if (Exceptions.findCause(ex, InterruptedException.class) == null) throw UiException.Aide.wrap(ex); // System.out.println(Thread.currentThread()+" interrupted silently: "+_ceased); } finally { --_nThd; synchronized (_evtmutex) { //just in case final boolean abnormal = _ceased == null; if (abnormal) _ceased = "Unknow reason"; _evtmutex.notify(); // if (abnormal) System.out.println(Thread.currentThread()+" stopped with unknown cause"); } } } /** Invokes {@link Configuration#newEventThreadCleanups}. */ private void newEventThreadCleanups(Configuration config, Throwable ex) { final List<Throwable> errs = new LinkedList<Throwable>(); if (ex != null) errs.add(ex); _evtThdCleanups = config.newEventThreadCleanups(getComponent(), getEvent(), errs, _ceased != null); _ex = errs.isEmpty() ? null : errs.get(0); //propagate back the first exception } /** Processes the component and event. */ private void process0() throws Exception { if (_proc == null) throw new IllegalStateException("Not initialized"); _proc.process(); } //-- Object --// public String toString() { return "[" + getName() + ": " + _proc + ", ceased=" + _ceased + ']'; } }