/* * Copyright (c) 2015 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of 'jMonkeyEngine' nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.jme3.network.util; import com.jme3.network.Message; import com.jme3.network.MessageConnection; import com.jme3.network.MessageListener; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; /** * A MessageListener implementation that will forward messages to methods * of a delegate object. These methods can be automapped or manually * specified. Subclasses provide specific implementations for how to * find the actual delegate object. * * @author Paul Speed */ public abstract class AbstractMessageDelegator<S extends MessageConnection> implements MessageListener<S> { static final Logger log = Logger.getLogger(AbstractMessageDelegator.class.getName()); private Class delegateType; private Map<Class, Method> methods = new HashMap<Class, Method>(); private Class[] messageTypes; /** * Creates an AbstractMessageDelegator that will forward received * messages to methods of the specified delegate type. If automap * is true then reflection is used to lookup probably message handling * methods. */ protected AbstractMessageDelegator( Class delegateType, boolean automap ) { this.delegateType = delegateType; if( automap ) { automap(); } } /** * Returns the array of messages known to be handled by this message * delegator. */ public Class[] getMessageTypes() { if( messageTypes == null ) { messageTypes = methods.keySet().toArray(new Class[methods.size()]); } return messageTypes; } /** * Returns true if the specified method is valid for the specified * message type. This is used internally during automapping to * provide implementation specific filting of methods. * This implementation checks for methods that take either the connection and message * type arguments (in that order) or just the message type. */ protected boolean isValidMethod( Method m, Class messageType ) { if( log.isLoggable(Level.FINEST) ) { log.log(Level.FINEST, "isValidMethod({0}, {1})", new Object[]{m, messageType}); } // Parameters must be S and message type or just message type Class<?>[] parms = m.getParameterTypes(); if( parms.length != 2 && parms.length != 1 ) { log.finest("Parameter count is not 1 or 2"); return false; } int messageIndex = 0; if( parms.length > 1 ) { if( MessageConnection.class.isAssignableFrom(parms[0]) ) { messageIndex++; } else { log.finest("First paramter is not a MessageConnection or subclass."); return false; } } if( messageType == null && !Message.class.isAssignableFrom(parms[messageIndex]) ) { log.finest("Second paramter is not a Message or subclass."); return false; } if( messageType != null && !parms[messageIndex].isAssignableFrom(messageType) ) { log.log(Level.FINEST, "Second paramter is not a {0}", messageType); return false; } return true; } /** * Convenience method that returns the message type as * reflecively determined for a particular method. This * only works with methods that actually have arguments. * This implementation returns the last element of the method's * getParameterTypes() array, thus supporting both * method(connection, messageType) as well as just method(messageType) * calling forms. */ protected Class getMessageType( Method m ) { Class<?>[] parms = m.getParameterTypes(); return parms[parms.length-1]; } /** * Goes through all of the delegate type's methods to find * a method of the specified name that may take the specified * message type. */ protected Method findDelegate( String name, Class messageType ) { // We do an exhaustive search because it's easier to // check for a variety of parameter types and it's all // that Class would be doing in getMethod() anyway. for( Method m : delegateType.getDeclaredMethods() ) { if( !m.getName().equals(name) ) { continue; } if( isValidMethod(m, messageType) ) { return m; } } return null; } /** * Returns true if the specified method name is allowed. * This is used by automapping to determine if a method * should be rejected purely on name. Default implemention * always returns true. */ protected boolean allowName( String name ) { return true; } /** * Calls the map(Set) method with a null argument causing * all available matching methods to mapped to message types. */ protected final void automap() { map((Set<String>)null); if( methods.isEmpty() ) { throw new RuntimeException("No message handling methods found for class:" + delegateType); } } /** * Specifically maps the specified methods names, autowiring * the parameters. */ public AbstractMessageDelegator<S> map( String... methodNames ) { Set<String> names = new HashSet<String>( Arrays.asList(methodNames) ); map(names); return this; } /** * Goes through all of the delegate type's declared methods * mapping methods that match the current constraints. * If the constraints set is null then allowName() is * checked for names otherwise only names in the constraints * set are allowed. * For each candidate method that passes the above checks, * isValidMethod() is called with a null message type argument. * All methods are made accessible thus supporting non-public * methods as well as public methods. */ protected void map( Set<String> constraints ) { if( log.isLoggable(Level.FINEST) ) { log.log(Level.FINEST, "map({0})", constraints); } for( Method m : delegateType.getDeclaredMethods() ) { if( log.isLoggable(Level.FINEST) ) { log.log(Level.FINEST, "Checking method:{0}", m); } if( constraints == null && !allowName(m.getName()) ) { log.finest("Name is not allowed."); continue; } if( constraints != null && !constraints.contains(m.getName()) ) { log.finest("Name is not in constraints set."); continue; } if( isValidMethod(m, null) ) { if( log.isLoggable(Level.FINEST) ) { log.log(Level.FINEST, "Adding method mapping:{0} = {1}", new Object[]{getMessageType(m), m}); } // Make sure we can access the method even if it's not public or // is in a non-public inner class. m.setAccessible(true); methods.put(getMessageType(m), m); } } messageTypes = null; } /** * Manually maps a specified method to the specified message type. */ public AbstractMessageDelegator<S> map( Class messageType, String methodName ) { // Lookup the method Method m = findDelegate( methodName, messageType ); if( m == null ) { throw new RuntimeException( "Method:" + methodName + " not found matching signature (MessageConnection, " + messageType.getName() + ")" ); } if( log.isLoggable(Level.FINEST) ) { log.log(Level.FINEST, "Adding method mapping:{0} = {1}", new Object[]{messageType, m}); } methods.put( messageType, m ); messageTypes = null; return this; } /** * Returns the mapped method for the specified message type. */ protected Method getMethod( Class c ) { Method m = methods.get(c); return m; } /** * Implemented by subclasses to provide the actual delegate object * against which the mapped message type methods will be called. */ protected abstract Object getSourceDelegate( S source ); /** * Implementation of the MessageListener's messageReceived() * method that will use the current message type mapping to * find an appropriate message handling method and call it * on the delegate returned by getSourceDelegate(). */ @Override public void messageReceived( S source, Message msg ) { if( msg == null ) { return; } Object delegate = getSourceDelegate(source); if( delegate == null ) { // Means ignore this message/source return; } Method m = getMethod(msg.getClass()); if( m == null ) { throw new RuntimeException("Delegate method not found for message class:" + msg.getClass()); } try { if( m.getParameterTypes().length > 1 ) { m.invoke( delegate, source, msg ); } else { m.invoke( delegate, msg ); } } catch( IllegalAccessException e ) { throw new RuntimeException("Error executing:" + m, e); } catch( InvocationTargetException e ) { throw new RuntimeException("Error executing:" + m, e.getCause()); } } }