|
@@ -0,0 +1,313 @@
|
|
|
+/*
|
|
|
+ * 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 no
|
|
|
+ * arguments, the connection and message type arguments (in that order),
|
|
|
+ * or just the message type or connection argument.
|
|
|
+ */
|
|
|
+ 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 connectionIndex = parms.length > 1 ? 0 : -1;
|
|
|
+ int messageIndex = parms.length > 1 ? 1 : 0;
|
|
|
+
|
|
|
+ if( connectionIndex > 0 && !MessageConnection.class.isAssignableFrom(parms[connectionIndex]) ) {
|
|
|
+ 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());
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|