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.experimental
parent
1fec72605f
commit
c9eaeeea12
@ -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()); |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
@ -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; |
||||
} |
||||
} |
||||
|
@ -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; |
||||
} |
||||
} |
||||
|
Loading…
Reference in new issue