Java tutorial
/* * polymap.org * Copyright 2012-2013, Falko Brutigam. All rights reserved. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.polymap.core.runtime.event; import java.util.EventObject; import java.util.Iterator; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.LinkedTransferQueue; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.eclipse.rwt.RWT; import org.eclipse.rwt.lifecycle.PhaseEvent; import org.eclipse.rwt.lifecycle.PhaseId; import org.eclipse.rwt.lifecycle.PhaseListener; import org.eclipse.rwt.lifecycle.UICallBack; import org.eclipse.rwt.service.ISessionStore; import org.eclipse.rwt.service.SessionStoreEvent; import org.eclipse.rwt.service.SessionStoreListener; import org.polymap.core.runtime.SessionContext; import org.polymap.core.runtime.Timer; /** * Provides the central API and implementation of the event system. Classes * interested in receiving events should use {@link EventHandler} to annotated the * handler methods. Event handler classes can then be * {@link #subscribe(Object, EventFilter...) subscribed} to the manager. * * @see EventHandler * @author <a href="http://www.polymap.de">Falko Brutigam</a> */ public class EventManager { private static Log log = LogFactory.getLog(EventManager.class); /** * The session that the currently dispatched event is published from. The * {@link SessionEventDispatcher event dispatcher} sets this for every dispatched * event. No {@link ThreadLocal} needed as there si just one thread dispatching * events. */ private static SessionContext threadPublishSession; private static final EventManager instance = new EventManager(); public static final EventManager instance() { return instance; } /** * The session this event was published from. * <p/> * This method can be called from within an event handler or filter method to * retrieve the session the current event was published from. * * @result The session, or null if published outside session. * @throws AssertionError If the method was called from outside an event handler * or filter method. */ public static SessionContext publishSession() { assert threadPublishSession != null; return threadPublishSession; } // instance ******************************************* private DispatcherThread dispatcher = new DispatcherThread(); private CopyOnWriteArraySet<AnnotatedEventListener> listeners = new CopyOnWriteArraySet(); private volatile int statCount; private Timer statTimer; private /*volatile*/ int pendingEvents; /** The global {@link PhaseListener} installed by the {@link SessionEventDispatcher}. */ private UICallbackPhaseListener phaseListener; protected EventManager() { // always keep one listener in the list so that SessionEventDispatcher // propery counts #pendingEvents subscribe(this, new EventFilter<EventObject>() { public boolean apply(EventObject input) { return false; } }); // install UICallbackPhaseListener try { // seems that a PhaseListener is installed just once for all sessions if (phaseListener == null) { phaseListener = new UICallbackPhaseListener(); RWT.getLifeCycle().addPhaseListener(phaseListener); } } catch (IllegalStateException e) { phaseListener = null; // outside request lifecycle -> no UICallback handling log.warn(e.toString()); } dispatcher.start(); } public void dispose() { if (dispatcher != null) { dispatcher.dispose(); dispatcher = null; } } @EventHandler protected void handleEvent(EventObject ev) { } /** * Asynchronously publish the given event. An event dispatch thread actually * delivers the events. This method may immediatelly return to the caller. * * @param ev The event to dispatch. */ public void publish(EventObject ev, Object... omitHandlers) { assert ev != null; Iterator<AnnotatedEventListener> snapshot = queueableListeners(); SessionEventDispatcher d = new SessionEventDispatcher(ev, snapshot, omitHandlers); dispatcher.dispatch(d); } /** * Synchronously publish the given event. This method will not return to the * caller until the event is dispatched to all listeners. * <p> * Using this method is discouraged. For normal event dispatch use the * asynchronous {@link #publish(EventObject)}. * * @see #publish(Event) * @param ev The event to dispatch. */ public void syncPublish(EventObject ev, Object... omitHandlers) { assert ev != null; Iterator<AnnotatedEventListener> snapshot = queueableListeners(); SessionEventDispatcher d = new SessionEventDispatcher(ev, snapshot, omitHandlers); dispatcher.dispatch(d); synchronized (d) { Timer timer = new Timer(); while (!d.isDone()) { try { d.wait(200); } catch (InterruptedException e) { } if (timer.elapsedTime() >= 5000) { throw new RuntimeException("Timeout exceeded for synch event: " + ev); } } } } /** * A snapshot of the current {@link #listeners}. */ protected Iterator<AnnotatedEventListener> queueableListeners() { return listeners.iterator(); } // /** // * <p/> // * Listeners are weakly referenced by the EventManager. A listener is reclaimed // * by the GC and removed from the EventManager as soon as there is no strong // * reference to it. An anonymous inner class can not be used as event listener. // * // * @param scope // * @param type // * @param listener // * @throws IllegalArgumentException If the given listener is registered already. // */ // public void subscribe( Event.Scope scope, Class<? extends EventObject> type, EventListener listener, EventFilter... filters ) { // // weak reference // Integer key = System.identityHashCode( listener ); // WeakListener chained = new WeakListener( listener, key ); // // // scope/type filter // TypeEventFilter typeFilter = new TypeEventFilter( type ); // ScopeEventFilter scopeFilter = ScopeEventFilter.forScope( scope ); // EventListener tweaked = new FilteringListener( chained, typeFilter, scopeFilter ); // // EventListener found = listeners.putIfAbsent( key, tweaked ); // if (found != null) { // throw new IllegalArgumentException( "EventListener already registered: " + listener ); // } // } /** * Registeres the given {@link EventHandler annotated} handler as event listener. * <p/> * Listeners are <b>weakly</b> referenced by the EventManager. A listener is * reclaimed by the GC and removed from the EventManager as soon as there is no * strong reference to it. An anonymous inner class can not be used as event * listener. * <p/> * The given handler and filters are called within the <b> * {@link SessionContext#current() current session}</b>. If the current method * call is done outside a session, then the handler is called with no session * set. A handler can use {@link EventManager#publishSession()} to retrieve the * session the event was published from. * * @see EventHandler * @param annotated The {@link EventHandler annotated} event handler. * @throws IllegalStateException If the handler is subscribed already. */ public void subscribe(Object annotated, EventFilter... filters) { assert annotated != null; Integer key = System.identityHashCode(annotated); AnnotatedEventListener listener = new AnnotatedEventListener(annotated, key, filters); if (!listeners.add(listener)) { throw new IllegalStateException("Event handler already registered: " + annotated); } } /** * * @param listenerOrHandler * @throws True if the given handler actually was removed. */ public boolean unsubscribe(Object annotated) { assert annotated != null; Integer key = System.identityHashCode(annotated); return removeKey(key) != null; } EventListener removeKey(Object key) { assert key instanceof Integer; EventListener removed = null; for (AnnotatedEventListener l : listeners) { if (l.getMapKey().equals(key)) { if (!listeners.remove(l)) { log.warn("Unable to remove key: " + key + " (EventManager: " + EventManager.instance().size() + ")"); } return l; } } log.warn("Unable to remove key: " + key + " (EventManager: " + EventManager.instance().size() + ")"); return null; } int size() { return listeners.size(); } /** * Checks if there are pending events after the render page of an request. If * yes, then UICallback is activated - until there are no pending events after * any subsequent request. * <p/> * XXX Currently #pendingEvents counts ALL events from all sessions! So a foreign * session might force a UICallback even if we don't have anything to render. */ private class UICallbackPhaseListener implements PhaseListener, SessionStoreListener { public PhaseId getPhaseId() { return PhaseId.ANY; } public void beforePhase(PhaseEvent ev) { //log.debug( "Before " + ev.getPhaseId() + ": pending=" + pendingEvents ); } public void afterPhase(PhaseEvent ev) { if (ev.getPhaseId() != PhaseId.PROCESS_ACTION) { return; } ISessionStore session = RWT.getSessionStore(); boolean uiCallbackActive = session.getAttribute("uiCallbackActive") != null; //log.debug( "After " + getPhaseId() + ": pending=" + pendingEvents + ", UICallbackActive=" + uiCallbackActive ); if (pendingEvents > 0) { if (pendingEvents > 0 && !uiCallbackActive) { log.debug("UICallback: ON (pending: " + pendingEvents + ")"); UICallBack.activate("EventManager.pendingEvents"); session.setAttribute("uiCallbackActive", true); } } else { if (uiCallbackActive) { log.debug("UICallback: OFF"); UICallBack.deactivate("EventManager.pendingEvents"); session.removeAttribute("uiCallbackActive"); } } } @Override public void beforeDestroy(SessionStoreEvent ev) { RWT.getLifeCycle().removePhaseListener(this); ev.getSessionStore().removeSessionStoreListener(this); } } /** * */ private class SessionEventDispatcher implements Runnable { private SessionContext publishSession; private Object[] omitHandlers; private Iterator<? extends EventListener> snapshot; private int dispatched; private EventObject event; private volatile boolean done; SessionEventDispatcher(EventObject event, Iterator<? extends EventListener> snapshot, Object[] omitHandlers) { this.event = event; this.snapshot = snapshot; this.omitHandlers = omitHandlers; this.publishSession = SessionContext.current(); // assert publishSession != null; assert omitHandlers != null; // XXX should never happen if (pendingEvents < 0) { //log.warn( "pendingEvents < 0 : " + pendingEvents, new Exception() ); pendingEvents = 0; } ++pendingEvents; } @Override public void run() { try { assert threadPublishSession == null; threadPublishSession = publishSession; while (snapshot.hasNext()) { try { EventListener listener = snapshot.next(); if (omitHandlers.length == 0 || !ArrayUtils.contains(omitHandlers, listener)) { listener.handleEvent(event); } } catch (Throwable e) { log.warn("Error during event dispatch: " + e, e); } } } finally { threadPublishSession = null; --pendingEvents; synchronized (this) { done = true; notifyAll(); } } } public boolean isDone() { return done; } @SuppressWarnings("hiding") protected void dispatchEvent(Object listener, Object listenerObject, int action, Object event) { // // statistics // if (log.isDebugEnabled()) { // statCount++; // if (statTimer == null) { // statTimer = new Timer(); // } // long elapsed = statTimer.elapsedTime(); // if (elapsed > 1000) { // log.debug( "********************************************** STATISTICS: " + statCount + " handlers/events in " + elapsed + "ms" ); // statCount = 0; // statTimer = null; // } // } } } /** * */ static class DispatcherThread extends Thread { public static final int MAX_QUEUE_SIZE = 10000; private BlockingQueue<Runnable> queue = new LinkedTransferQueue(); //ArrayBlockingQueue( MAX_QUEUE_SIZE ); /** * Non synchronized "assumption" about size of the {@link #queue}. * XXX on IA32 it seems to work ok without "volatile"; not sure about other platforms; * see http://brooker.co.za/blog/2012/09/10/volatile.html */ private int queueSize; private int queueReadCount; private boolean stopped; public DispatcherThread() { super("EventManager.Dispatcher"); //setPriority( Thread.MAX_PRIORITY ); setDaemon(true); } public void dispose() { stopped = true; } public void dispatch(Runnable work) { try { for (int i = 0; queueSize >= MAX_QUEUE_SIZE; i++) { log.trace("Waiting on dispatch... queue: " + queueSize); try { Thread.sleep(Math.min(10 * i, 1000)); } catch (InterruptedException e) { } ; log.trace(" queue: " + queueSize); } ++queueSize; //log.debug( "Queue size: " + queue.size() ); queue.put(work); } catch (InterruptedException e) { throw new RuntimeException(e); } } @Override public void run() { while (!stopped) { try { if (queueReadCount > MAX_QUEUE_SIZE) { // synchronize the assumption with real value queueSize = queue.size(); queueReadCount = 0; } ++queueReadCount; Runnable work = queue.take(); work.run(); --queueSize; } catch (InterruptedException e) { } } } } }