/* This file is part of leafdigital leafChat. leafChat is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. leafChat 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 General Public License for more details. You should have received a copy of the GNU General Public License along with leafChat. If not, see <http://www.gnu.org/licenses/>. Copyright 2011 Samuel Marshall. */ package leafchat.core; import java.lang.reflect.*; import java.util.*; import javax.swing.SwingUtilities; import util.ReflectionUtils; import leafchat.core.api.*; /** Provides system behaviour for message dispatching etc. */ public class MessageManager { // Handle singleton behaviour (note: as this is not accessible to plugins, // it doesn't use the SingletonManager features) /** Single instance */ private static MessageManager mm=new MessageManager(); /** @return Singleton instance */ public static MessageManager get() { return mm; } /** Private constructor to prevent separate construction */ private MessageManager() { registerMessageClass(Msg.class); } // Actual implementation /** Map of message Class -> OwnerDetails */ private Map<Class<? extends Msg>, OwnerDetails> owners = new HashMap<Class<? extends Msg>, OwnerDetails>(); /** Map of Class(target) -> (Map of Class(message) -> Method) */ private Map<Class<?>, Map<Class<? extends Msg>, Method>> classHandlerMethods = new HashMap<Class<?>, Map<Class<? extends Msg>, Method>>(); /** Map from Class => MessageInfo */ private HashMap<Class<?>, MessageInfo> messageInfo=new HashMap<Class<?>, MessageInfo>(); /** Debug method that lists current requests */ public void displayHandlers() { synchronized(owners) { System.err.println("Message requests"); System.err.println("===="); for(Map.Entry<Class<? extends Msg>, OwnerDetails> me : owners.entrySet()) { Class<? extends Msg> cMessage = me.getKey(); OwnerDetails od = me.getValue(); System.err.println("\n"+cMessage.getName()+"\n----"); for(Iterator<?> iRequest=od.requests.iterator();iRequest.hasNext();) { RequestDetails rd=(RequestDetails)iRequest.next(); System.err.println(rd); } } System.err.println("====end"); } } /** * Registers a new message owner. This also automatically registers the * main message class. You need to manually register any subclasses. * @param owner Owner object * @throws BugException If message owner is already registered */ public void registerOwner(MsgOwner owner) throws BugException { Class<? extends Msg> msgClass = owner.getMessageClass(); if(!msgClass.getName().endsWith("Msg")) throw new BugException( "Message class must end with 'Msg': "+msgClass.getName()); registerMessageClass(msgClass); synchronized(owners) { if(owners.containsKey(msgClass)) throw new BugException( "A MessageOwner "+msgClass.getName()+" is already registered"); if(msgClass.isInterface()) throw new BugException( "Message types must be classes, not interfaces"); OwnerDetails od=new OwnerDetails(owner); owners.put(msgClass,od); } owner.init(new MessageDispatchProvider(this,msgClass)); } /** * Removes a registered message owner, along with all its registered requests. * @param owner Owner object * @throws GeneralException If message owner is not registered */ public void unregisterOwner(MsgOwner owner) throws GeneralException { Class<? extends Msg> msgClass = owner.getMessageClass(); unregisterMessageClass(msgClass); synchronized(owners) { if(!owners.containsKey(msgClass)) throw new GeneralException( "The MessageOwner for "+msgClass.getName()+" is not registered"); owners.remove(msgClass); } } /** * Registers a new message class. You do not need to call this for message * classes that are directly returned by {@link MsgOwner#getMessageClass()}. * @param c Message class * @throws BugException If class is already registered or if class * is illegal for some reason. */ public void registerMessageClass(Class<? extends Msg> c) { MessageInfo info=null; try { info=(MessageInfo)c.getDeclaredField("info"). get(null); if(info.getMessageClass()!=c) throw new BugException("Message class "+c+" has MessageInfo for other class"); } catch(NoSuchFieldException e) { // Message class doesn't provide info, so use default info=new MessageInfo(c); } catch(Throwable t) // A huge range of other possible errors { throw new BugException(t); } synchronized(messageInfo) { if(messageInfo.containsKey(c)) throw new BugException("The message "+c+" is already registered"); messageInfo.put(c,info); Class<?> superclass=info.getMessageClass().getSuperclass(); MessageInfo superclassInfo=messageInfo.get(superclass); if(superclassInfo!=null) { superclassInfo.addSubclass(info); info.setSuperclass(superclassInfo); } else { if(Msg.class.isAssignableFrom(superclass)) { throw new BugException("The message "+c+" was registered before its superclass, "+superclass); } } } } /** * Unregisters a message class. You do not need to call this for the main * {@link MsgOwner#getMessageClass()}. * @param c Class to unregister * @throws BugException If class is not registered */ public void unregisterMessageClass(Class<? extends Msg> c) { synchronized(messageInfo) { MessageInfo info=messageInfo.remove(c); if(info==null) throw new BugException("The message "+c+" is not registered"); info.close(); } } /** * Obtains the MessageInfo object for a particular message class. * @param c Message class * @return Info * @throws BugException If you request MessageInfo for a class that doesn't * have it */ public MessageInfo getMessageInfo(Class<? extends Msg> c) { synchronized(messageInfo) { MessageInfo info=messageInfo.get(c); if(info==null) throw new BugException("No message info for class "+c); return info; } } /** * Requests message notification for the given owner class. * @param c Message class * @param target Target for notifications * @param mf Filter for notifications (may be null) * @param priority Request priority * @return ID of request * @throws BugException If classname doesn't exist */ public int requestMessages(Class<? extends Msg> c, Object target, MessageFilter mf, int priority) throws BugException { MsgOwner mo; int iRequestID; Collection<RequestDetails> cRequests; synchronized(owners) { OwnerDetails od=null; // Check there is an owner that handles these messages Class<? extends Msg> cProvided=c; while(cProvided != null) { od = owners.get(cProvided); if(od!=null) { break; } if(cProvided.equals(Msg.class)) { break; } cProvided = cProvided.getSuperclass().asSubclass(Msg.class); } if(od == null) { throw new BugException( "No registered MessageOwner provides the message " + c.getName()); } // Get handler methods for the class and check one of them implements this // message Map<Class<? extends Msg>, Method> mHandlers = getHandlerMethods(target.getClass()); Class<? extends Msg> cHandledMessage=c; while(true) { // See if there's a handler for this if(mHandlers.containsKey(cHandledMessage)) { // OK, good we have a handler break; } // Stop when we get to the message root if(cHandledMessage==Msg.class) { throw new BugException("Object class "+target.getClass().getName()+ " cannot handle requested messages "+c.getName()+" (add a msg method)"); } cHandledMessage = ReflectionUtils.getSuperclassOrInterface( cHandledMessage).asSubclass(Msg.class); } // Get ID from owner od.lastId++; iRequestID=od.lastId; mo=od.mo; cRequests=od.requests; } // See if the system is supposed to handle these messages if(mo.registerTarget(target,c,mf,iRequestID,priority)) { RequestDetails rd=new RequestDetails(target,c,mf,iRequestID, priority); synchronized(cRequests) { cRequests.add(rd); } } return iRequestID; } /** * Called when a plugin is unloaded. Any items cached by message manager * have to be cleared now. * @param pcl Classloader that's going */ void clearCachedItems(PluginClassLoader pcl) { synchronized(classHandlerMethods) { for(Iterator<Class<?>> i = classHandlerMethods.keySet().iterator(); i.hasNext();) { Class<?> c = i.next(); if(c.getClassLoader()==pcl) { i.remove(); } } } } /** * Obtains (from cache or by working it out) a map of message handler methods in * the given class. * @param c Target class * @return Map from Class (message class) -> Method * @throws BugException Any errors in the class */ private Map<Class<? extends Msg>, Method> getHandlerMethods( Class<?> c) throws BugException { synchronized(classHandlerMethods) { Map<Class<? extends Msg>, Method> mHandlers = classHandlerMethods.get(c); if(mHandlers==null) { // Build list of msg methods in requester mHandlers = new HashMap<Class<? extends Msg>, Method>(); classHandlerMethods.put(c, mHandlers); Method[] am=c.getMethods(); for(int iMethod=0;iMethod<am.length;iMethod++) { // Consider only methods called msg Method m=am[iMethod]; if(!m.getName().equals("msg")) continue; // Check modifiers int iMods=m.getModifiers(); String sError=c.getName()+"."+m.getName(); if(!Modifier.isPublic(iMods)) throw new BugException( "Message handler must be public: "+ sError); if(Modifier.isStatic(iMods)) throw new BugException( "Message handler may not be static: "+ sError); // Get message class Class<?>[] ac = m.getParameterTypes(); if(ac.length!=1) throw new BugException( "Message handler must take single parameter: "+ sError); if(!(Msg.class.isAssignableFrom(ac[0]))) throw new BugException( "Message handler parameter must be a Msg subclass: "+ sError); // Add to list mHandlers.put(ac[0].asSubclass(Msg.class), m); } } return mHandlers; } } /** * Removes request notification with given ID. * <p> * This method ignores requests that aren't found, rather than throwing an * exception, because it's possible that requests are removed automatically * by the removal of a requestowner. * @param c Message class * @param target Target requesting removal * @param requestId ID of request */ public void unrequestMessages(Class<? extends Msg> c, Object target, int requestId) { MsgOwner owner; Collection<RequestDetails> requests; // Find owner synchronized(owners) { OwnerDetails details=null; Class<? extends Msg> realMessageClass=c; while(true) { details = owners.get(realMessageClass); if(details!=null) { break; } if(realMessageClass == Msg.class) { break; } realMessageClass = realMessageClass.getSuperclass().asSubclass(Msg.class); } if(details==null) return; // Ignore if owner no longer exists owner = details.mo; requests = details.requests; } // Check if request is stored in our list and, if so, remove it from that synchronized(requests) { for(Iterator<RequestDetails> i=requests.iterator(); i.hasNext();) { RequestDetails rd = i.next(); if(rd.requestId==requestId && rd.target==target) { i.remove(); break; } } } // Tell the owner it's been removed owner.unregisterTarget(target,requestId); } /** * Dispatch a message from outside the MessageOwner that owns it. Checks * with the MessageOwner first. * @param c Message class * @param m Actual message * @param immediate True if message should be sent to all targets * immediately before this message returns; false if it should be queued * for later sending after other messages have been handled * @return True if message was permitted, false otherwise * @throws BugException If the message is of invalid type or the * MessageOwner cannot be found */ public boolean externalDispatch(Class<? extends Msg> c, Msg m, boolean immediate) throws BugException { MsgOwner mo; synchronized(owners) { OwnerDetails od=owners.get(c); if(od==null) throw new BugException( "No registered MessageOwner provides the message "+c.getName()); mo=od.mo; } if(mo.allowExternalDispatch(m)) { dispatch(c,m, immediate); return true; } else return false; } /** * @return True if in the event thread */ public static boolean isEventThread() { return SwingUtilities.isEventDispatchThread(); } /** * Dispatch a message. * @param c Message class * @param m Actual message * @param immediate True if message should be sent to all targets * immediately before this message returns; false if it should be queued * for later sending after other messages have been handled * @throws BugException If the message is of invalid type or the * MessageOwner cannot be found */ void dispatch(final Class<? extends Msg> c, final Msg m, boolean immediate) throws BugException { if(!SwingUtilities.isEventDispatchThread() && immediate) throw new BugException("Must be in Swing thread for immediate despatch"); if(immediate) { internalDispatch(c,m); } else { PendingEvent event=new PendingEvent(c,m); synchronized(pendingEvents) { // Put event into list. Event goes at the end, except that if // there are any existing events that are sequenced to happen after // it, these are pulled out and put afterwards. LinkedList<PendingEvent> after=new LinkedList<PendingEvent>(); for(Iterator<PendingEvent> i=pendingEvents.iterator();i.hasNext();) { PendingEvent other=i.next(); if(event.m.sequenceBefore(other.m)) { i.remove(); after.add(other); } } pendingEvents.add(event); pendingEvents.addAll(after); if(pendingEvents.size()==1) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { flush(); } }); } } } } /** Event information on queue */ class PendingEvent { Class<? extends Msg> messageClass; Msg m; public PendingEvent(Class<? extends Msg> messageClass,Msg m) { this.messageClass=messageClass; this.m=m; } } /** * Runs through all pending events until the list is empty. */ public void flush() { while(true) { PendingEvent first; synchronized(pendingEvents) { if(pendingEvents.isEmpty()) break; first=pendingEvents.removeFirst(); } try { internalDispatch(first.messageClass,first.m); } catch(Throwable t) { ErrorMsg.report( "Error during dispatch of "+first.m,t); } } } /** * Runs a given task after the current event has finished processing * and the event queue is empty. * @param r Task to run */ public void yield(final Runnable r) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { synchronized(pendingEvents) { if(pendingEvents.isEmpty()) r.run(); else yield(r); } } }); } private LinkedList<PendingEvent> pendingEvents = new LinkedList<PendingEvent>(); /** * Handles actual message dispatch. * @param c Message class * @param m Message */ private void internalDispatch(Class<? extends Msg> c, Msg m) { // Verify class if(!c.isAssignableFrom(m.getClass())) throw new BugException( "Message "+m+" is not of the claimed type "+c.getName()); // Find owner MsgOwner owner; Collection<RequestDetails> requests; synchronized(owners) { OwnerDetails details = owners.get(c); if(details==null) { throw new BugException( "No registered MessageOwner provides the message "+c.getName()); } owner = details.mo; requests = details.requests; } // Allow owner chance to do its own dispatch owner.manualDispatch(m); // Get list of targets for system dispatch RequestDetails[] ard; synchronized(requests) { // Doing just this is safer, so we don't stay synchronized while // processing messages ard=requests.toArray(new RequestDetails[0]); } // Dispatch to all targets for (int iTarget= 0; iTarget < ard.length; iTarget++) { if(m.isStopped()) break; RequestDetails rd=ard[iTarget]; if( (rd.msgClass==null || rd.msgClass.isAssignableFrom(m.getClass())) && (rd.mf==null || rd.mf.accept(m))) { dispatchMessageToTarget(m,rd); } } } /** * Dispatchs a message to a single target. * @param m Message to dispatch * @param o Target to receive it */ public void dispatchMessageToTarget(Msg m, Object o) { dispatchMessageToTarget(m, new RequestDetails(o,null,null,-1,-1)); } /** * Dispatchs a message to a single target. * @param m Message to dispatch * @param rd Details of target to receive it */ private void dispatchMessageToTarget(Msg m, RequestDetails rd) { try { Map<?, ?> mHandlers = getHandlerMethods(rd.target.getClass()); Class<? extends Msg> cHandledMessage = m.getClass(); while(true) { // See if there's a handler for this Method mHandler=(Method)mHandlers.get(cHandledMessage); if(mHandler!=null) { // OK, good we have a handler mHandler.invoke(rd.target,new Object[]{m}); break; } // Stop when we get to the message root if(cHandledMessage==Msg.class) { throw new BugException("Object class "+rd.target.getClass().getName()+ " cannot handle requested messages "+m.getClass().getName()); } cHandledMessage = ReflectionUtils.getSuperclassOrInterface( cHandledMessage).asSubclass(Msg.class); } } catch(Throwable t) { // Don't let an exception in message processing stop the other // targets from receiving the message; instead, report it through // the errorhandler message. // Special-case if this *is* the errorhandler message... if(m instanceof ErrorMsg) { t.printStackTrace(); } else { // Ok, make a new error message and dispatch it ErrorMsg.report( "Error during dispatch of "+m+" to "+rd,t); } } } static class DispatchItem { Class<? extends Msg> msgClass; Msg m; DispatchItem(Class<? extends Msg> msgClass, Msg m) { this.msgClass = msgClass; this.m=m; } } static class OwnerDetails { OwnerDetails(MsgOwner mo) { this.mo=mo; } MsgOwner mo; int lastId = 0; Set<RequestDetails> requests = new TreeSet<RequestDetails>(); } static class RequestDetails implements Comparable<RequestDetails> { Object target; MessageFilter mf; Class<? extends Msg> msgClass; int requestId; int priority; RequestDetails(Object oTarget, Class<? extends Msg> cMessage, MessageFilter mf,int iRequestID, int iPriority) { this.target=oTarget; this.msgClass=cMessage; this.mf=mf; this.requestId=iRequestID; this.priority=iPriority; } @Override public int compareTo(RequestDetails other) { if(other==this) return 0; if(priority > other.priority) { return -1; // This should come earlier } else if(priority < other.priority) { return 1; // This should come later } else // Lower request IDs come earlier { return requestId - other.requestId; } } @Override public String toString() { return "["+msgClass.getName()+"] -> "+target+" (pri "+priority+",id "+requestId+")"; } } }