Sfoglia il codice sorgente

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.

Paul Speed 10 anni fa
parent
commit
c9eaeeea12

+ 313 - 0
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<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());
+        }
+    }
+}
+
+

+ 72 - 0
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<S extends MessageConnection> extends AbstractMessageDelegator<S> {
+ 
+    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.
+     *  <p>Methods of the following signatures are allowed:
+     *  <ul>
+     *  <li>void someName(S conn, SomeMessage msg)
+     *  <li>void someName(Message msg)
+     *  </ul>
+     *  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;
+    }
+}
+

+ 103 - 0
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<HostedConnection> {
+ 
+    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.
+     *  <p>Methods of the following signatures are allowed:
+     *  <ul>
+     *  <li>void someName(S conn, SomeMessage msg)
+     *  <li>void someName(Message msg)
+     *  </ul>
+     *  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;
+    }
+}
+