From c9eaeeea12f68e5d88e4142cc0c9ac51f88ef5ff Mon Sep 17 00:00:00 2001 From: Paul Speed Date: Sat, 25 Apr 2015 23:47:26 -0400 Subject: [PATCH] Added some message delegator util classes that makes it easier to handle network messages. These delegators can introspect a delegate type to find message-type specific handler methods. This mapping can be done automatically or performed manually. --- .../util/AbstractMessageDelegator.java | 313 ++++++++++++++++++ .../network/util/ObjectMessageDelegator.java | 72 ++++ .../network/util/SessionDataDelegator.java | 103 ++++++ 3 files changed, 488 insertions(+) create mode 100644 jme3-networking/src/main/java/com/jme3/network/util/AbstractMessageDelegator.java create mode 100644 jme3-networking/src/main/java/com/jme3/network/util/ObjectMessageDelegator.java create mode 100644 jme3-networking/src/main/java/com/jme3/network/util/SessionDataDelegator.java diff --git a/jme3-networking/src/main/java/com/jme3/network/util/AbstractMessageDelegator.java b/jme3-networking/src/main/java/com/jme3/network/util/AbstractMessageDelegator.java new file mode 100644 index 000000000..b077f9e01 --- /dev/null +++ b/jme3-networking/src/main/java/com/jme3/network/util/AbstractMessageDelegator.java @@ -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 + implements MessageListener { + + static final Logger log = Logger.getLogger(AbstractMessageDelegator.class.getName()); + + private Class delegateType; + private Map methods = new HashMap(); + 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)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 map( String... methodNames ) { + Set names = new HashSet( 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 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 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()); + } + } +} + + diff --git a/jme3-networking/src/main/java/com/jme3/network/util/ObjectMessageDelegator.java b/jme3-networking/src/main/java/com/jme3/network/util/ObjectMessageDelegator.java new file mode 100644 index 000000000..b92ee09a8 --- /dev/null +++ b/jme3-networking/src/main/java/com/jme3/network/util/ObjectMessageDelegator.java @@ -0,0 +1,72 @@ +/* + * 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.MessageConnection; + + +/** + * A MessageListener implementation that will forward messages to methods + * of a specified delegate object. These methods can be automapped or manually + * specified. + * + * @author Paul Speed + */ +public class ObjectMessageDelegator extends AbstractMessageDelegator { + + private Object delegate; + + /** + * Creates a MessageListener that will forward mapped message types + * to methods of the specified object. + * If automap is true then all methods with the proper signature will + * be mapped. + *

Methods of the following signatures are allowed: + *

    + *
  • void someName(S conn, SomeMessage msg) + *
  • void someName(Message msg) + *
+ * Where S is the type of MessageConnection and SomeMessage is some + * specific concreate Message subclass. + */ + public ObjectMessageDelegator( Object delegate, boolean automap ) { + super(delegate.getClass(), automap); + this.delegate = delegate; + } + + @Override + protected Object getSourceDelegate( MessageConnection source ) { + return delegate; + } +} + diff --git a/jme3-networking/src/main/java/com/jme3/network/util/SessionDataDelegator.java b/jme3-networking/src/main/java/com/jme3/network/util/SessionDataDelegator.java new file mode 100644 index 000000000..f2bee3373 --- /dev/null +++ b/jme3-networking/src/main/java/com/jme3/network/util/SessionDataDelegator.java @@ -0,0 +1,103 @@ +/* + * 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.HostedConnection; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * A MessageListener implementation that will forward messages to methods + * of a delegate specified as a HostedConnection session attribute. This is + * useful for handling connection-specific messages from clients that must + * delegate to client-specific data objects. + * The delegate methods can be automapped or manually specified. + * + * @author Paul Speed + */ +public class SessionDataDelegator extends AbstractMessageDelegator { + + static final Logger log = Logger.getLogger(SessionDataDelegator.class.getName()); + + private String attributeName; + + /** + * Creates a MessageListener that will forward mapped message types + * to methods of an object specified as a HostedConnection attribute. + * If automap is true then all methods with the proper signature will + * be mapped. + *

Methods of the following signatures are allowed: + *

    + *
  • void someName(S conn, SomeMessage msg) + *
  • void someName(Message msg) + *
+ * Where S is the type of MessageConnection and SomeMessage is some + * specific concreate Message subclass. + */ + public SessionDataDelegator( Class delegateType, String attributeName, boolean automap ) { + super(delegateType, automap); + this.attributeName = attributeName; + } + + /** + * Returns the attribute name that will be used to look up the + * delegate object. + */ + public String getAttributeName() { + return attributeName; + } + + /** + * Called internally when there is no session object + * for the current attribute name attached to the passed source + * HostConnection. Default implementation logs a warning. + */ + protected void miss( HostedConnection source ) { + log.log(Level.WARNING, "Session data is null for:{0} on connection:{1}", new Object[]{attributeName, source}); + } + + /** + * Returns the attributeName attribute of the supplied source + * HostConnection. If there is no value at that attribute then + * the miss() method is called. + */ + protected Object getSourceDelegate( HostedConnection source ) { + Object result = source.getAttribute(attributeName); + if( result == null ) { + miss(source); + } + return result; + } +} +