Merge branch 'master' into PBRisComing

define_list_fix
Nehon 9 years ago
commit aa067ef60d
  1. 4
      jme3-android/src/main/java/com/jme3/system/android/JmeAndroidSystem.java
  2. 1
      jme3-core/src/main/java/com/jme3/font/BitmapTextPage.java
  3. 32
      jme3-core/src/main/java/com/jme3/scene/Node.java
  4. 4
      jme3-core/src/main/java/com/jme3/system/JmeSystemDelegate.java
  5. 5
      jme3-core/src/main/java/com/jme3/system/Platform.java
  6. 5
      jme3-core/src/plugins/java/com/jme3/texture/plugins/HDRLoader.java
  7. 80
      jme3-examples/src/main/java/jme3test/network/TestChatClient.java
  8. 74
      jme3-examples/src/main/java/jme3test/network/TestChatClientAndServer.java
  9. 113
      jme3-examples/src/main/java/jme3test/network/TestChatServer.java
  10. 22
      jme3-networking/src/main/java/com/jme3/network/message/SerializerRegistrationsMessage.java
  11. 56
      jme3-networking/src/main/java/com/jme3/network/service/rmi/Asynchronous.java
  12. 60
      jme3-networking/src/main/java/com/jme3/network/service/rmi/CallType.java
  13. 112
      jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfo.java
  14. 104
      jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfoRegistry.java
  15. 137
      jme3-networking/src/main/java/com/jme3/network/service/rmi/MethodInfo.java
  16. 87
      jme3-networking/src/main/java/com/jme3/network/service/rmi/RemoteObjectHandler.java
  17. 195
      jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiClientService.java
  18. 60
      jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiContext.java
  19. 262
      jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiHostedService.java
  20. 387
      jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiRegistry.java
  21. 20
      jme3-networking/src/main/java/com/jme3/network/service/serializer/ClientSerializerRegistrationsService.java

@ -122,9 +122,13 @@ public class JmeAndroidSystem extends JmeSystemDelegate {
return Platform.Android_ARM6; return Platform.Android_ARM6;
} else if (arch.contains("v7")) { } else if (arch.contains("v7")) {
return Platform.Android_ARM7; return Platform.Android_ARM7;
} else if (arch.contains("v8")) {
return Platform.Android_ARM8;
} else { } else {
return Platform.Android_ARM5; // unknown ARM return Platform.Android_ARM5; // unknown ARM
} }
} else if (arch.contains("aarch")) {
return Platform.Android_ARM8;
} else { } else {
return Platform.Android_Other; return Platform.Android_Other;
} }

@ -59,6 +59,7 @@ class BitmapTextPage extends Geometry {
BitmapTextPage(BitmapFont font, boolean arrayBased, int page) { BitmapTextPage(BitmapFont font, boolean arrayBased, int page) {
super("BitmapFont", new Mesh()); super("BitmapFont", new Mesh());
setRequiresUpdates(false);
setBatchHint(BatchHint.Never); setBatchHint(BatchHint.Never);
if (font == null) { if (font == null) {
throw new IllegalArgumentException("font cannot be null."); throw new IllegalArgumentException("font cannot be null.");

@ -195,7 +195,7 @@ public class Node extends Spatial {
void invalidateUpdateList() { void invalidateUpdateList() {
updateListValid = false; updateListValid = false;
if ( parent != null ) { if ( parent != null ) {
parent.invalidateUpdateList(); parent.invalidateUpdateList();
} }
} }
@ -570,6 +570,35 @@ public class Node extends Spatial {
// optimization: try collideWith BoundingVolume to avoid possibly redundant tests on children // optimization: try collideWith BoundingVolume to avoid possibly redundant tests on children
// number 4 in condition is somewhat arbitrary. When there is only one child, the boundingVolume test is redundant at all. // number 4 in condition is somewhat arbitrary. When there is only one child, the boundingVolume test is redundant at all.
// The idea is when there are few children, it can be too expensive to test boundingVolume first. // The idea is when there are few children, it can be too expensive to test boundingVolume first.
/*
I'm removing this change until some issues can be addressed and I really
think it needs to be implemented a better way anyway.
First, it causes issues for anyone doing collideWith() with BoundingVolumes
and expecting it to trickle down to the children. For example, children
with BoundingSphere bounding volumes and collideWith(BoundingSphere). Doing
a collision check at the parent level then has to do a BoundingSphere to BoundingBox
collision which isn't resolved. (Having to come up with a collision point in that
case is tricky and the first sign that this is the wrong approach.)
Second, the rippling changes this caused to 'optimize' collideWith() for this
special use-case are another sign that this approach was a bit dodgy. The whole
idea of calculating a full collision just to see if the two shapes collide at all
is very wasteful.
A proper implementation should support a simpler boolean check that doesn't do
all of that calculation. For example, if 'other' is also a BoundingVolume (ie: 99.9%
of all non-Ray cases) then a direct BV to BV intersects() test can be done. So much
faster. And if 'other' _is_ a Ray then the BV.intersects(Ray) call can be done.
I don't have time to do it right now but I'll at least un-break a bunch of peoples'
code until it can be 'optimized' properly. Hopefully it's not too late to back out
the other dodgy ripples this caused. -pspeed (hindsight-expert ;))
Note: the code itself is relatively simple to implement but I don't have time to
a) test it, and b) see if '> 4' is still a decent check for it. Could be it's fast
enough to do all the time for > 1.
if (children.size() > 4) if (children.size() > 4)
{ {
BoundingVolume bv = this.getWorldBound(); BoundingVolume bv = this.getWorldBound();
@ -578,6 +607,7 @@ public class Node extends Spatial {
// collideWith without CollisionResults parameter used to avoid allocation when possible // collideWith without CollisionResults parameter used to avoid allocation when possible
if (bv.collideWith(other) == 0) return 0; if (bv.collideWith(other) == 0) return 0;
} }
*/
for (Spatial child : children.getArray()){ for (Spatial child : children.getArray()){
total += child.collideWith(other, results); total += child.collideWith(other, results);
} }

@ -153,6 +153,10 @@ public abstract class JmeSystemDelegate {
return false; return false;
} else if (arch.equals("universal")) { } else if (arch.equals("universal")) {
return false; return false;
} else if (arch.equals("aarch32")) {
return false;
} else if (arch.equals("aarch64")) {
return true;
} else if (arch.equals("arm")) { } else if (arch.equals("arm")) {
return false; return false;
} else { } else {

@ -88,6 +88,11 @@ public enum Platform {
*/ */
Android_ARM7, Android_ARM7,
/**
* Android ARM8
*/
Android_ARM8,
/** /**
* Android x86 * Android x86
*/ */

@ -308,9 +308,8 @@ public class HDRLoader implements AssetLoader {
} }
in.close(); in.close();
dataStore.rewind(); dataStore.rewind();
//TODO, HDR color space? considered linear here return new Image(pixelFormat, width, height, dataStore, ColorSpace.sRGB);
return new Image(pixelFormat, width, height, dataStore, ColorSpace.Linear);
} }
public Object load(AssetInfo info) throws IOException { public Object load(AssetInfo info) throws IOException {

@ -32,6 +32,8 @@
package jme3test.network; package jme3test.network;
import com.jme3.network.Client; import com.jme3.network.Client;
import com.jme3.network.ClientStateListener;
import com.jme3.network.ErrorListener;
import com.jme3.network.Message; import com.jme3.network.Message;
import com.jme3.network.MessageListener; import com.jme3.network.MessageListener;
import com.jme3.network.Network; import com.jme3.network.Network;
@ -51,11 +53,11 @@ import jme3test.network.TestChatServer.ChatMessage;
*/ */
public class TestChatClient extends JFrame { public class TestChatClient extends JFrame {
private Client client; private final Client client;
private JEditorPane chatLog; private final JEditorPane chatLog;
private StringBuilder chatMessages = new StringBuilder(); private final StringBuilder chatMessages = new StringBuilder();
private JTextField nameField; private final JTextField nameField;
private JTextField messageField; private final JTextField messageField;
public TestChatClient(String host) throws IOException { public TestChatClient(String host) throws IOException {
super("jME3 Test Chat Client - to:" + host); super("jME3 Test Chat Client - to:" + host);
@ -90,7 +92,20 @@ public class TestChatClient extends JFrame {
client = Network.connectToServer(TestChatServer.NAME, TestChatServer.VERSION, client = Network.connectToServer(TestChatServer.NAME, TestChatServer.VERSION,
host, TestChatServer.PORT, TestChatServer.UDP_PORT); host, TestChatServer.PORT, TestChatServer.UDP_PORT);
client.addMessageListener(new ChatHandler(), ChatMessage.class); client.addMessageListener(new ChatHandler(), ChatMessage.class);
client.addClientStateListener(new ChatClientStateListener());
client.addErrorListener(new ChatErrorListener());
client.start(); client.start();
System.out.println("Started client:" + client);
}
@Override
public void dispose() {
System.out.println("Chat window closing.");
super.dispose();
if( client.isConnected() ) {
client.close();
}
} }
public static String getString(Component owner, String title, String message, String initialValue) { public static String getString(Component owner, String title, String message, String initialValue) {
@ -99,7 +114,12 @@ public class TestChatClient extends JFrame {
} }
public static void main(String... args) throws Exception { public static void main(String... args) throws Exception {
TestChatServer.initializeClasses();
// Note: in JME 3.1 this is generally unnecessary as the server will
// send a message with all server-registered classes.
// TestChatServer.initializeClasses();
// Leaving the call commented out to be illustrative regarding the
// common old pattern.
// Grab a host string from the user // Grab a host string from the user
String s = getString(null, "Host Info", "Enter chat host:", "localhost"); String s = getString(null, "Host Info", "Enter chat host:", "localhost");
@ -108,12 +128,23 @@ public class TestChatClient extends JFrame {
return; return;
} }
// Register a shutdown hook to get a message on the console when the
// app actually finishes
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
System.out.println("Chat client is terminating.");
}
});
TestChatClient test = new TestChatClient(s); TestChatClient test = new TestChatClient(s);
test.setVisible(true); test.setVisible(true);
} }
private class ChatHandler implements MessageListener<Client> { private class ChatHandler implements MessageListener<Client> {
@Override
public void messageReceived(Client source, Message m) { public void messageReceived(Client source, Message m) {
ChatMessage chat = (ChatMessage) m; ChatMessage chat = (ChatMessage) m;
@ -134,15 +165,50 @@ public class TestChatClient extends JFrame {
} }
} }
private class ChatClientStateListener implements ClientStateListener {
@Override
public void clientConnected(Client c) {
System.out.println("clientConnected(" + c + ")");
}
@Override
public void clientDisconnected(Client c, DisconnectInfo info) {
System.out.println("clientDisconnected(" + c + "):" + info);
if( info != null ) {
// The connection was closed by the server
JOptionPane.showMessageDialog(rootPane,
info.reason,
"Connection Closed",
JOptionPane.INFORMATION_MESSAGE);
dispose();
}
}
}
private class ChatErrorListener implements ErrorListener<Client> {
@Override
public void handleError( Client source, Throwable t ) {
System.out.println("handleError(" + source + ", " + t + ")");
JOptionPane.showMessageDialog(rootPane,
String.valueOf(t),
"Connection Error",
JOptionPane.ERROR_MESSAGE);
}
}
private class SendAction extends AbstractAction { private class SendAction extends AbstractAction {
private boolean reliable; private final boolean reliable;
public SendAction(boolean reliable) { public SendAction(boolean reliable) {
super(reliable ? "TCP" : "UDP"); super(reliable ? "TCP" : "UDP");
this.reliable = reliable; this.reliable = reliable;
} }
@Override
public void actionPerformed(ActionEvent evt) { public void actionPerformed(ActionEvent evt) {
String name = nameField.getText(); String name = nameField.getText();
String message = messageField.getText(); String message = messageField.getText();

@ -0,0 +1,74 @@
/*
* 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 jme3test.network;
/**
* Combines the server instance and a client instance into the
* same JVM to show an example of, and to test, a pattern like
* self-hosted multiplayer games.
*
* @author Paul Speed
*/
public class TestChatClientAndServer {
public static void main( String... args ) throws Exception {
System.out.println("Starting chat server...");
TestChatServer chatServer = new TestChatServer();
chatServer.start();
System.out.println("Waiting for connections on port:" + TestChatServer.PORT);
// Now launch a client
TestChatClient test = new TestChatClient("localhost");
test.setVisible(true);
// Register a shutdown hook to get a message on the console when the
// app actually finishes
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
System.out.println("Client and server test is terminating.");
}
});
// Keep running basically forever or until the server
// shuts down
while( chatServer.isRunning() ) {
synchronized (chatServer) {
chatServer.wait();
}
}
}
}

@ -34,6 +34,7 @@ package jme3test.network;
import com.jme3.network.*; import com.jme3.network.*;
import com.jme3.network.serializing.Serializable; import com.jme3.network.serializing.Serializable;
import com.jme3.network.serializing.Serializer; import com.jme3.network.serializing.Serializer;
import java.io.IOException;
/** /**
* A simple test chat server. When SM implements a set * A simple test chat server. When SM implements a set
@ -51,51 +52,134 @@ public class TestChatServer {
public static final int PORT = 5110; public static final int PORT = 5110;
public static final int UDP_PORT = 5110; public static final int UDP_PORT = 5110;
public static void initializeClasses() { private Server server;
// Doing it here means that the client code only needs to private boolean isRunning;
// call our initialize.
Serializer.registerClass(ChatMessage.class); public TestChatServer() throws IOException {
}
public static void main(String... args) throws Exception {
initializeClasses(); initializeClasses();
// Use this to test the client/server name version check // Use this to test the client/server name version check
Server server = Network.createServer(NAME, VERSION, PORT, UDP_PORT); this.server = Network.createServer(NAME, VERSION, PORT, UDP_PORT);
server.start();
ChatHandler handler = new ChatHandler(); ChatHandler handler = new ChatHandler();
server.addMessageListener(handler, ChatMessage.class); server.addMessageListener(handler, ChatMessage.class);
server.addConnectionListener(new ChatConnectionListener());
}
public boolean isRunning() {
return isRunning;
}
public synchronized void start() {
if( isRunning ) {
return;
}
server.start();
isRunning = true;
}
public synchronized void close() {
if( !isRunning ) {
return;
}
// Gracefully let any connections know that the server is
// going down. Without this, their connections will simply
// error out.
for( HostedConnection conn : server.getConnections() ) {
conn.close("Server is shutting down.");
}
try {
Thread.sleep(1000); // wait a couple beats to let the messages go out
} catch( InterruptedException e ) {
e.printStackTrace();
}
server.close();
isRunning = false;
notifyAll();
}
protected void runCommand( HostedConnection conn, String user, String command ) {
if( "/shutdown".equals(command) ) {
server.broadcast(new ChatMessage("server", "Server is shutting down."));
close();
} else if( "/help".equals(command) ) {
StringBuilder sb = new StringBuilder();
sb.append("Chat commands:\n");
sb.append("/help - prints this message.\n");
sb.append("/shutdown - shuts down the server.");
server.broadcast(new ChatMessage("server", sb.toString()));
}
}
public static void initializeClasses() {
// Doing it here means that the client code only needs to
// call our initialize.
Serializer.registerClass(ChatMessage.class);
}
public static void main(String... args) throws Exception {
TestChatServer chatServer = new TestChatServer();
chatServer.start();
System.out.println("Waiting for connections on port:" + PORT);
// Keep running basically forever // Keep running basically forever
synchronized (NAME) { while( chatServer.isRunning ) {
NAME.wait(); synchronized (chatServer) {
chatServer.wait();
}
} }
} }
private static class ChatHandler implements MessageListener<HostedConnection> { private class ChatHandler implements MessageListener<HostedConnection> {
public ChatHandler() { public ChatHandler() {
} }
@Override
public void messageReceived(HostedConnection source, Message m) { public void messageReceived(HostedConnection source, Message m) {
if (m instanceof ChatMessage) { if (m instanceof ChatMessage) {
// Keep track of the name just in case we // Keep track of the name just in case we
// want to know it for some other reason later and it's // want to know it for some other reason later and it's
// a good example of session data // a good example of session data
source.setAttribute("name", ((ChatMessage) m).getName()); ChatMessage cm = (ChatMessage)m;
source.setAttribute("name", cm.getName());
// Check for a / command
if( cm.message.startsWith("/") ) {
runCommand(source, cm.name, cm.message);
return;
}
System.out.println("Broadcasting:" + m + " reliable:" + m.isReliable()); System.out.println("Broadcasting:" + m + " reliable:" + m.isReliable());
// Just rebroadcast... the reliable flag will stay the // Just rebroadcast... the reliable flag will stay the
// same so if it came in on UDP it will go out on that too // same so if it came in on UDP it will go out on that too
source.getServer().broadcast(m); source.getServer().broadcast(cm);
} else { } else {
System.err.println("Received odd message:" + m); System.err.println("Received odd message:" + m);
} }
} }
} }
private class ChatConnectionListener implements ConnectionListener {
@Override
public void connectionAdded( Server server, HostedConnection conn ) {
System.out.println("connectionAdded(" + conn + ")");
}
@Override
public void connectionRemoved(Server server, HostedConnection conn) {
System.out.println("connectionRemoved(" + conn + ")");
}
}
@Serializable @Serializable
public static class ChatMessage extends AbstractMessage { public static class ChatMessage extends AbstractMessage {
@ -126,6 +210,7 @@ public class TestChatServer {
return message; return message;
} }
@Override
public String toString() { public String toString() {
return name + ":" + message; return name + ":" + message;
} }

@ -94,7 +94,7 @@ public class SerializerRegistrationsMessage extends AbstractMessage {
public static Registration[] compiled; public static Registration[] compiled;
private static final Serializer fieldSerializer = new FieldSerializer(); private static final Serializer fieldSerializer = new FieldSerializer();
private Registration[] registrations; private Registration[] registrations;
public SerializerRegistrationsMessage() { public SerializerRegistrationsMessage() {
setReliable(true); setReliable(true);
@ -132,7 +132,25 @@ public class SerializerRegistrationsMessage extends AbstractMessage {
Serializer.setReadOnly(true); Serializer.setReadOnly(true);
} }
public void registerAll() { public void registerAll() {
// See if we will have problems because our registry is locked
if( Serializer.isReadOnly() ) {
// Check to see if maybe we are executing this from the
// same JVM that sent the message, ie: client and server are running on
// the same JVM
// There could be more advanced checks than this but for now we'll
// assume that if the registry was compiled here then it means
// we are also the server process. Note that this wouldn't hold true
// under complicated examples where there are clients of one server
// that also run their own servers but realistically they would have
// to disable the ServerSerializerRegistrationsServer anyway.
if( compiled != null ) {
log.log( Level.INFO, "Skipping registration as registry is locked, presumably by a local server process.");
return;
}
}
for( Registration reg : registrations ) { for( Registration reg : registrations ) {
log.log( Level.INFO, "Registering:{0}", reg); log.log( Level.INFO, "Registering:{0}", reg);
reg.register(); reg.register();

@ -0,0 +1,56 @@
/*
* 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.service.rmi;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;
/**
* Indicates that a given method should be executed asynchronously
* through the RMI service. This must annotate the method on the
* shared interface for it to have an effect. If reliable=false
* is specified then remote method invocation is done over UDP
* instead of TCP, ie: unreliably... but faster.
*
* @author Paul Speed
*/
@Retention(value=RUNTIME)
@Target(value=METHOD)
public @interface Asynchronous {
boolean reliable() default true;
}

@ -0,0 +1,60 @@
/*
* 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.service.rmi;
/**
* Internal type denoting the type of call to make when remotely
* invoking methods.
*
* @author Paul Speed
*/
public enum CallType {
/**
* Caller will block until a response is received and returned.
*/
Synchronous,
/**
* Caller does not block or wait for a response. The other end
* of the connection will also not send one.
*/
Asynchronous,
/**
* Similar to asynchronous in that no response is expected or sent
* but differs in that the call will be sent over UDP and so may
* not make it to the other end.
*/
Unreliable
}

@ -0,0 +1,112 @@
/*
* 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.service.rmi;
import com.jme3.network.serializing.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/**
* Internal information about a shared class. This is the information
* that is sent over the wire for shared types.
*
* @author Paul Speed
*/
@Serializable
public final class ClassInfo {
private String name;
private short typeId;
private MethodInfo[] methods;
/**
* For serialization only.
*/
public ClassInfo() {
}
public ClassInfo( short typeId, Class type ) {
this.typeId = typeId;
this.name = type.getName();
this.methods = toMethodInfo(type, type.getMethods());
}
public String getName() {
return name;
}
public Class getType() {
try {
return Class.forName(name);
} catch( ClassNotFoundException e ) {
throw new RuntimeException("Error finding class for:" + this, e);
}
}
public short getId() {
return typeId;
}
public MethodInfo getMethod( short id ) {
return methods[id];
}
public MethodInfo getMethod( Method m ) {
for( MethodInfo mi : methods ) {
if( mi.matches(m) ) {
return mi;
}
}
return null;
}
private MethodInfo[] toMethodInfo( Class type, Method[] methods ) {
List<MethodInfo> result = new ArrayList<MethodInfo>();
short methodId = 0;
for( Method m : methods ) {
// Simple... add all methods exposed through the interface
result.add(new MethodInfo(methodId++, m));
}
return result.toArray(new MethodInfo[result.size()]);
}
public MethodInfo[] getMethods() {
return methods;
}
@Override
public String toString() {
return "ClassInfo[" + name + "]";
}
}

@ -0,0 +1,104 @@
/*
* 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.service.rmi;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* Internal registry of shared types and their ClassInfo and MethodInfo
* objects.
*
* @author Paul Speed
*/
public class ClassInfoRegistry {
//private final LoadingCache<Class, ClassInfo> cache; // Guava version
private final Map<Class, ClassInfo> cache = new HashMap<Class, ClassInfo>();
private final AtomicInteger nextClassId = new AtomicInteger();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public ClassInfoRegistry() {
//this.cache = CacheBuilder.newBuilder().build(new ClassInfoLoader()); // Guava version
}
public ClassInfo getClassInfo( Class type ) {
//return cache.getUnchecked(type); // Guava version
// More complicated without guava
lock.readLock().lock();
try {
ClassInfo result = cache.get(type);
if( result != null ) {
return result;
}
// Else we need to create it and store it... so grab the write
// lock
lock.readLock().unlock();
lock.writeLock().lock();
try {
// Note: it's technically possible that a race with another thread
// asking for the same class already created one between our read unlock
// and our write lock. No matter as it's cheap to create one and does
// no harm. Code is simpler without the double-check.
result = new ClassInfo((short)nextClassId.getAndIncrement(), type);
cache.put(type, result);
// Regrab the read lock before leaving... kind of unnecessary but
// it makes the method cleaner and widens the gap of lock races.
// Downgrading a write lock to read is ok.
lock.readLock().lock();
return result;
} finally {
// Unlock the write lock while still holding onto read
lock.writeLock().unlock();
}
} finally {
lock.readLock().unlock();
}
}
/*
would be more straight-forward with guava Guava version
private class ClassInfoLoader extends CacheLoader<Class, ClassInfo> {
@Override
public ClassInfo load( Class type ) {
return new ClassInfo((short)nextClassId.getAndIncrement(), type);
}
}*/
}

@ -0,0 +1,137 @@
/*
* 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.service.rmi;
import com.jme3.network.serializing.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import javax.jws.Oneway;
/**
* Internal information about shared methods. This is part of the data that
* is passed over the wire when an object is shared.
*
* @author Paul Speed
*/
@Serializable
public final class MethodInfo {
public static final MethodInfo NULL_INFO = new MethodInfo();
private String representation;
private short id;
private CallType callType;
private transient Method method;
/**
* For serialization only.
*/
public MethodInfo() {
}
public MethodInfo( short id, Method m ) {
this.id = id;
this.method = m;
this.representation = methodToString(m);
this.callType = getCallType(m);
}
public Object invoke( Object target, Object... parms ) {
try {
return method.invoke(target, parms);
} catch (IllegalAccessException e) {
throw new RuntimeException("Error invoking:" + method + " on:" + target, e);
} catch (IllegalArgumentException e) {
throw new RuntimeException("Error invoking:" + method + " on:" + target, e);
} catch (InvocationTargetException e) {
throw new RuntimeException("Error invoking:" + method + " on:" + target, e);
}
}
public short getId() {
return id;
}
public CallType getCallType() {
return callType;
}
public boolean matches( Method m ) {
return representation.equals(methodToString(m));
}
public static String methodToString( Method m ) {
StringBuilder sb = new StringBuilder();
for( Class t : m.getParameterTypes() ) {
if( sb.length() > 0 )
sb.append(", ");
sb.append(t.getName());
}
return m.getReturnType().getName() + " " + m.getName() + "(" + sb + ")";
}
public static CallType getCallType( Method m ) {
if( m.getReturnType() != Void.TYPE )
return CallType.Synchronous;
if( m.getAnnotation(Oneway.class) != null )
return CallType.Asynchronous;
if( m.getAnnotation(Asynchronous.class) == null )
return CallType.Synchronous;
Asynchronous async = m.getAnnotation(Asynchronous.class);
return async.reliable() ? CallType.Asynchronous : CallType.Unreliable;
}
@Override
public int hashCode() {
return representation.hashCode();
}
@Override
public boolean equals( Object o ) {
if( o == this ) {
return true;
}
if( o == null || o.getClass() != getClass() ) {
return false;
}
MethodInfo other = (MethodInfo)o;
return representation.equals(other.representation);
}
@Override
public String toString() {
return "MethodInfo[#" + getId() + ", callType=" + callType + ", " + representation + "]";
}
}

@ -0,0 +1,87 @@
/*
* 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.service.rmi;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Used internally to remotely invoke methods on RMI shared objects.
*
* @author Paul Speed
*/
public class RemoteObjectHandler implements InvocationHandler {
private final RmiRegistry rmi;
private final byte channel;
private final short objectId;
private final ClassInfo typeInfo;
private final Map<Method, MethodInfo> methodIndex = new ConcurrentHashMap<Method, MethodInfo>();
public RemoteObjectHandler( RmiRegistry rmi, byte channel, short objectId, ClassInfo typeInfo ) {
this.rmi = rmi;
this.channel = channel;
this.objectId = objectId;
this.typeInfo = typeInfo;
}
protected MethodInfo getMethodInfo( Method method ) {
MethodInfo mi = methodIndex.get(method);
if( mi == null ) {
mi = typeInfo.getMethod(method);
if( mi == null ) {
mi = MethodInfo.NULL_INFO;
}
methodIndex.put(method, mi);
}
return mi == MethodInfo.NULL_INFO ? null : mi;
}
@Override
public Object invoke(Object o, Method method, Object[] os) throws Throwable {
MethodInfo mi = getMethodInfo(method);
if( mi == null ) {
// Try to invoke locally
return method.invoke(this, os);
}
return rmi.invokeRemote(channel, objectId, mi.getId(), mi.getCallType(), os);
}
@Override
public String toString() {
return "RemoteObject[#" + objectId + ", " + typeInfo.getName() + "]";
}
}

@ -0,0 +1,195 @@
/*
* 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.service.rmi;
import com.jme3.network.MessageConnection;
import com.jme3.network.service.AbstractClientService;
import com.jme3.network.service.ClientServiceManager;
import com.jme3.network.service.rpc.RpcClientService;
import java.util.ArrayList;
import java.util.List;
/**
* A service that can be added to the client to support a simple
* shared objects protocol.
*
* <p>Objects are shared by adding them to the RmiRegistry with one of the
* share() methods. Shared objects must have a separate interface and implementation.
* The interface is what the other end of the connection will use to interact
* with the object and that interface class must be available on both ends of
* the connection. The implementing class need only be on the sharing end.</p>
*
* <p>Shared objects can be accessed on the other end of the connection by
* using one of the RmiRegistry's getRemoteObject() methods. These can be
* used to lookup an object by class if it is a shared singleton or by name
* if it was registered with a name.</p>
*
* <p>Note: This RMI implementation is not as advanced as Java's regular
* RMI as it won't marshall shared references, ie: you can't pass
* a shared objects as an argument to another shared object's method.</p>
*
* @author Paul Speed
*/
public class RmiClientService extends AbstractClientService {
private RpcClientService rpc;
private byte defaultChannel;
private short rmiObjectId;
private RmiRegistry rmi;
private volatile boolean isStarted = false;
private final List<ObjectInfo> pending = new ArrayList<ObjectInfo>();
public RmiClientService() {
this((short)-1, (byte)MessageConnection.CHANNEL_DEFAULT_RELIABLE);
}
public RmiClientService( short rmiObjectId, byte defaultChannel ) {
this.defaultChannel = defaultChannel;
this.rmiObjectId = rmiObjectId;
}
/**
* Shares the specified object with the server and associates it with the
* specified type. Objects shared in this way are available in the connection-specific
* RMI registry on the server and are not available to other connections.
*/
public <T> void share( T object, Class<? super T> type ) {
share(defaultChannel, object, type);
}
/**
* Shares the specified object with the server and associates it with the
* specified type. Objects shared in this way are available in the connection-specific
* RMI registry on the server and are not available to other connections.
* All object related communication will be done over the specified connection
* channel.
*/
public <T> void share( byte channel, T object, Class<? super T> type ) {
share(channel, type.getName(), object, type);
}
/**
* Shares the specified object with the server and associates it with the
* specified name. Objects shared in this way are available in the connection-specific
* RMI registry on the server and are not available to other connections.
*/
public <T> void share( String name, T object, Class<? super T> type ) {
share(defaultChannel, name, object, type);
}
/**
* Shares the specified object with the server and associates it with the
* specified name. Objects shared in this way are available in the connection-specific
* RMI registry on the server and are not available to other connections.
* All object related communication will be done over the specified connection
* channel.
*/
public <T> void share( byte channel, String name, T object, Class<? super T> type ) {
if( !isStarted ) {
synchronized(pending) {
if( !isStarted ) {
pending.add(new ObjectInfo(channel, name, object, type));
return;
}
}
}
// Else we can add it directly.
rmi.share(channel, name, object, type);
}
/**
* Looks up a remote object on the server by type and returns a local proxy to the
* remote object that was shared on the other end of the network connection.
*/
public <T> T getRemoteObject( Class<T> type ) {
return rmi.getRemoteObject(type);
}
/**
* Looks up a remote object on the server by name and returns a local proxy to the
* remote object that was shared on the other end of the network connection.
*/
public <T> T getRemoteObject( String name, Class<T> type ) {
return rmi.getRemoteObject(name, type);
}
@Override
protected void onInitialize( ClientServiceManager s ) {
rpc = getService(RpcClientService.class);
if( rpc == null ) {
throw new RuntimeException("RmiClientService requires RpcClientService");
}
// Register it now so that it is available when the
// server starts to send us stuff. Waiting until start()
// is too late in this case.
rmi = new RmiRegistry(rpc.getRpcConnection(), rmiObjectId, defaultChannel);
}
@Override
public void start() {
super.start();
// Register all of the classes that have been waiting.
synchronized(pending) {
for( ObjectInfo info : pending ) {
rmi.share(info.channel, info.name, info.object, info.type);
}
pending.clear();
isStarted = true;
}
}
private class ObjectInfo {
byte channel;
String name;
Object object;
Class type;
public ObjectInfo( byte channel, String name, Object object, Class type ) {
this.channel = channel;
this.name = name;
this.object = object;
this.type = type;
}
@Override
public String toString() {
return "ObjectInfo[" + channel + ", " + name + ", " + object + ", " + type + "]";
}
}
}

@ -0,0 +1,60 @@
/*
* 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.service.rmi;
import com.jme3.network.HostedConnection;
/**
* Keeps track of the current connection performing a particular
* RMI call. RMI-based services can use this to find out which
* connection is calling a particular method without having to
* pass additional problematic data on the method calls.
*
* @author Paul Speed
*/
public class RmiContext {
private static final ThreadLocal<HostedConnection> connection = new ThreadLocal<HostedConnection>();
/**
* Returns the HostedConnection that is responsible for any
* RMI-related calls on this thread.
*/
public static HostedConnection getRmiConnection() {
return connection.get();
}
static void setRmiConnection( HostedConnection conn ) {
connection.set(conn);
}
}

@ -0,0 +1,262 @@
/*
* 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.service.rmi;
import com.jme3.network.HostedConnection;
import com.jme3.network.MessageConnection;
import com.jme3.network.Server;
import com.jme3.network.serializing.Serializer;
import com.jme3.network.service.AbstractHostedService;
import com.jme3.network.service.HostedServiceManager;
import com.jme3.network.service.rpc.RpcHostedService;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A service that can be added to the host to support a simple
* shared objects protocol.
*
* <p>Objects are shared by adding them to the RmiRegistry with one of the
* share() methods. Shared objects must have a separate interface and implementation.
* The interface is what the other end of the connection will use to interact
* with the object and that interface class must be available on both ends of
* the connection. The implementing class need only be on the sharing end.</p>
*
* <p>Shared objects can be accessed on the other end of the connection by
* using one of the RmiRegistry's getRemoteObject() methods. These can be
* used to lookup an object by class if it is a shared singleton or by name
* if it was registered with a name.</p>
*
* <p>On the hosting side, a special shardGlobal() method is provided that
* will register shared objects that will automatically be provided to every
* new joining client and they will all be calling the same server-side instance.
* Normally, shared objects themselves are connection specific and handled
* at the connection layer. The shareGlobal() space is a way to have global
* resources passed directly though the need is relatively rare.</p>
*
* <p>Note: This RMI implementation is not as advanced as Java's regular
* RMI as it won't marshall shared references, ie: you can't pass
* a shared objects as an argument to another shared object's method.</p>
*
* @author Paul Speed
*/
public class RmiHostedService extends AbstractHostedService {
static final Logger log = Logger.getLogger(RpcHostedService.class.getName());
public static final String ATTRIBUTE_NAME = "rmi";
private RpcHostedService rpcService;
private short rmiId;
private byte defaultChannel;
private boolean autoHost;
private final Map<String, GlobalShare> globalShares = new ConcurrentHashMap<String, GlobalShare>();
public RmiHostedService() {
this((short)-1, (byte)MessageConnection.CHANNEL_DEFAULT_RELIABLE, true);
}
public RmiHostedService( short rmiId, byte defaultChannel, boolean autoHost ) {
this.rmiId = rmiId;
this.defaultChannel = defaultChannel;
this.autoHost = autoHost;
Serializer.registerClasses(ClassInfo.class, MethodInfo.class);
}
/**
* Shares a server-wide object associated with the specified type. All connections
* with RMI hosting started will have access to this shared object as soon as they
* connect and they will all share the same instance. It is up to the shared object
* to handle any multithreading that might be required.
*/
public <T> void shareGlobal( T object, Class<? super T> type ) {
shareGlobal(defaultChannel, type.getName(), object, type);
}
/**
* Shares a server-wide object associated with the specified name. All connections
* with RMI hosting started will have access to this shared object as soon as they
* connect and they will all share the same instance. It is up to the shared object
* to handle any multithreading that might be required.
*/
public <T> void shareGlobal( String name, T object, Class<? super T> type ) {
shareGlobal(defaultChannel, name, object, type);
}
/**
* Shares a server-wide object associated with the specified name over the specified
* channel. All connections with RMI hosting started will have access to this shared
* object as soon as they connect and they will all share the same instance. It is up
* to the shared object to handle any multithreading that might be required.
* All network communcation associated with the shared object will be done over
* the specified channel.
*/
public <T> void shareGlobal( byte channel, String name, T object, Class<? super T> type ) {
GlobalShare share = new GlobalShare(channel, object, type);
GlobalShare existing = globalShares.put(name, share);
if( existing != null ) {
// Shouldn't need to do anything actually.
}
// Go through all of the children
for( HostedConnection conn : getServer().getConnections() ) {
RmiRegistry child = getRmiRegistry(conn);
if( child == null ) {
continue;
}
child.share(channel, name, object, type);
}
}
/**
* Set to true if all new connections should automatically have RMI hosting started.
* Set to false if the game-specific connection setup will call startHostingOnConnection()
* after some connection setup is done (for example, logging in). Note: generally
* is is safe to autohost RMI as long as callers are careful about what they've added
* using shareGlobal(). One reasonable use-case is to shareGlobal() some kind of login
* service and nothing else. All other shared objects would then be added as connection
* specific objects during successful login processing.
*/
public void setAutoHost( boolean b ) {
this.autoHost = b;
}
/**
* Returns true if RMI hosting is automatically started for all new connections.
*/
public boolean getAutoHost() {
return autoHost;
}
/**
* Returns the RMI registry for the specific HostedConection. Each connection
* has its own registry with its own connection-specific shared objects.
*/
public RmiRegistry getRmiRegistry( HostedConnection hc ) {
return hc.getAttribute(ATTRIBUTE_NAME);
}
/**
* Sets up RMI hosting services for the hosted connection allowing
* getRmiRegistry() to return a valid RmiRegistry object.
* This method is called automatically for all new connections if
* autohost is set to true.
*/
public void startHostingOnConnection( HostedConnection hc ) {
if( log.isLoggable(Level.FINEST) ) {
log.log(Level.FINEST, "startHostingOnConnection:{0}", hc);
}
RmiRegistry rmi = new RmiRegistry(hc, rpcService.getRpcConnection(hc),
rmiId, defaultChannel);
hc.setAttribute(ATTRIBUTE_NAME, rmi);
// Register any global shares
for( Map.Entry<String, GlobalShare> e : globalShares.entrySet() ) {
GlobalShare share = e.getValue();
rmi.share(share.channel, e.getKey(), share.object, share.type);
}
}
/**
* Removes any RMI hosting services associated with the specified
* connection. Calls to getRmiRegistry() will return null for
* this connection.
* This method is called automatically for all leaving connections if
* autohost is set to true.
*/
public void stopHostingOnConnection( HostedConnection hc ) {
RmiRegistry rmi = hc.getAttribute(ATTRIBUTE_NAME);
if( rmi == null ) {
return;
}
if( log.isLoggable(Level.FINEST) ) {
log.log(Level.FINEST, "stopHostingOnConnection:{0}", hc);
}
hc.setAttribute(ATTRIBUTE_NAME, null);
//rpc.close();
}
@Override
protected void onInitialize( HostedServiceManager s ) {
this.rpcService = getService(RpcHostedService.class);
if( rpcService == null ) {
throw new RuntimeException("RmiHostedService requires RpcHostedService");
}
}
/**
* Called internally when a new connection is detected for
* the server. If the current autoHost property is true then
* startHostingOnConnection(hc) is called.
*/
@Override
public void connectionAdded(Server server, HostedConnection hc) {
if( log.isLoggable(Level.FINEST) ) {
log.log(Level.FINEST, "connectionAdded({0}, {1})", new Object[]{server, hc});
}
if( autoHost ) {
startHostingOnConnection(hc);
}
}
/**
* Called internally when an existing connection is leaving
* the server. If the current autoHost property is true then
* stopHostingOnConnection(hc) is called.
*/
@Override
public void connectionRemoved(Server server, HostedConnection hc) {
if( log.isLoggable(Level.FINEST) ) {
log.log(Level.FINEST, "connectionRemoved({0}, {1})", new Object[]{server, hc});
}
if( autoHost ) {
stopHostingOnConnection(hc);
}
}
private class GlobalShare {
byte channel;
Object object;
Class type;
public GlobalShare( byte channel, Object object, Class type ) {
this.channel = channel;
this.object = object;
this.type = type;
}
}
}

@ -0,0 +1,387 @@
/*
* 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.service.rmi;
import com.jme3.network.HostedConnection;
import com.jme3.network.MessageConnection;
import com.jme3.network.service.rpc.RpcConnection;
import com.jme3.network.service.rpc.RpcHandler;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
*
* @author Paul Speed
*/
public class RmiRegistry {
static final Logger log = Logger.getLogger(RmiRegistry.class.getName());
// RPC IDs for calling our remote endpoint
private static final short NEW_CLASS = 0;
private static final short ADD_OBJECT = 1;
private static final short REMOVE_OBJECT = 2;
private RpcConnection rpc;
private short rmiId;
private byte defaultChannel;
private final RmiHandler rmiHandler = new RmiHandler();
private final ClassInfoRegistry classCache = new ClassInfoRegistry();
private final AtomicInteger nextObjectId = new AtomicInteger();
private final ObjectIndex<SharedObject> local = new ObjectIndex<SharedObject>();
private final ObjectIndex<Object> remote = new ObjectIndex<Object>();
// Only used on the server to provide thread-local context for
// local RMI calls.
private HostedConnection context;
public RmiRegistry( RpcConnection rpc, short rmiId, byte defaultChannel ) {
this(null, rpc, rmiId, defaultChannel);
}
public RmiRegistry( HostedConnection context, RpcConnection rpc, short rmiId, byte defaultChannel ) {
this.context = context;
this.rpc = rpc;
this.rmiId = rmiId;
this.defaultChannel = defaultChannel;
rpc.registerHandler(rmiId, rmiHandler);
}
/**
* Exposes the specified object to the other end of the connection as
* the specified interface type. The object can be looked up by type
* on the other end.
*/
public <T> void share( T object, Class<? super T> type ) {
share(defaultChannel, object, type);
}
/**
* Exposes, through a specific connection channel, the specified object
* to the other end of the connection as the specified interface type.
* The object can be looked up by type on the other end.
* The specified channel will be used for all network communication
* specific to this object.
*/
public <T> void share( byte channel, T object, Class<? super T> type ) {
share(channel, type.getName(), object, type);
}
/**
* Exposes the specified object to the other end of the connection as
* the specified interface type and associates it with the specified name.
* The object can be looked up by the associated name on the other end of
* the connection.
*/
public <T> void share( String name, T object, Class<? super T> type ) {
share(defaultChannel, name, object, type);
}
/**
* Exposes, through a specific connection channel, the specified object to
* the other end of the connection as the specified interface type and associates
* it with the specified name.
* The object can be looked up by the associated name on the other end of
* the connection.
* The specified channel will be used for all network communication
* specific to this object.
*/
public <T> void share( byte channel, String name, T object, Class<? super T> type ) {
ClassInfo typeInfo = classCache.getClassInfo(type);
local.lock.writeLock().lock();
try {
// First see if we've told the remote end about this class
// before
if( local.classes.put(typeInfo.getId(), typeInfo) == null ) {
// It's new
rpc.callAsync(defaultChannel, rmiId, NEW_CLASS, typeInfo);
// Because type info IDs are global to the class cache,
// we could in theory keep a global index that we broadcast
// on first connection setup... we need only prepopulate
// the index in that case.
}
// See if we already shared an object under that name
SharedObject existing = local.byName.remove(name);
if( existing != null ) {
local.byId.remove(existing.objectId);
rpc.removeHandler(existing.objectId, rmiHandler);
// Need to delete the old one from the remote end
rpc.callAsync(defaultChannel, rmiId, REMOVE_OBJECT, existing.objectId);
// We don't reuse the ID because it's kind of dangerous.
// Churning through a new ID is our safety net for accidents.
}
SharedObject newShare = new SharedObject(name, object, type, typeInfo);
local.byName.put(name, newShare);
local.byId.put(newShare.objectId, newShare);
// Make sure we are setup to receive the remote method calls through
// the RPC service
rpc.registerHandler(newShare.objectId, rmiHandler);
// Let the other end know
rpc.callAsync(defaultChannel, rmiId, ADD_OBJECT, channel, newShare.objectId, name, typeInfo.getId());
// We send the ADD_OBJECT to the other end before releasing the
// lock to avoid a potential inconsistency if two threads try to
// jam the same name at the same time. Otherwise, if the timing were
// right, the remove for one object could get there before its add.
} finally {
local.lock.writeLock().unlock();
}
}
/**
* Returns a local object that was previously registered with share() using
* just type registration.
*/
public <T> T getLocalObject( Class<T> type ) {
return getLocalObject(type.getName(), type);
}
/**
* Returns a local object that was previously registered with share() using
* name registration.
*/
public <T> T getLocalObject( String name, Class<T> type ) {
local.lock.readLock().lock();
try {
return type.cast(local.byName.get(name));
} finally {
local.lock.readLock().unlock();
}
}
/**
* Looks up a remote object by type and returns a local proxy to the remote object
* that was shared on the other end of the network connection. If this is called
* from a client then it is accessing a shared object registered on the server.
* If this is called from the server then it is accessing a shared object registered
* on the client.
*/
public <T> T getRemoteObject( Class<T> type ) {
return getRemoteObject(type.getName(), type);
}
/**
* Looks up a remote object by name and returns a local proxy to the remote object
* that was shared on the other end of the network connection. If this is called
* from a client then it is accessing a shared object registered on the server.
* If this is called from the server then it is accessing a shared object registered
* on the client.
*/
public <T> T getRemoteObject( String name, Class<T> type ) {
remote.lock.readLock().lock();
try {
return type.cast(remote.byName.get(name));
} finally {
remote.lock.readLock().unlock();
}
}
protected void addRemoteClass( ClassInfo info ) {
if( remote.classes.put(info.getId(), info) != null ) {
throw new RuntimeException("Error class already exists for ID:" + info.getId());
}
}
protected void removeRemoteObject( short objectId ) {
if( log.isLoggable(Level.FINEST) ) {
log.log(Level.FINEST, "removeRemoteObject({0})", objectId);
}
throw new UnsupportedOperationException("Removal not yet implemented.");
}
protected void addRemoteObject( byte channel, short objectId, String name, ClassInfo typeInfo ) {
if( log.isLoggable(Level.FINEST) ) {
log.finest("addRemoveObject(" + objectId + ", " + name + ", " + typeInfo + ")");
}
remote.lock.writeLock().lock();
try {
Object existing = remote.byName.get(name);
if( existing != null ) {
throw new RuntimeException("Object already registered for:" + name);
}
RemoteObjectHandler remoteHandler = new RemoteObjectHandler(this, channel, objectId, typeInfo);
Object remoteObject = Proxy.newProxyInstance(getClass().getClassLoader(),
new Class[] {typeInfo.getType()},
remoteHandler);
remote.byName.put(name, remoteObject);
remote.byId.put(objectId, remoteObject);
} finally {
remote.lock.writeLock().unlock();
}
}
protected Object invokeRemote( byte channel, short objectId, short procId, CallType callType, Object[] args ) {
if( log.isLoggable(Level.FINEST) ) {
log.finest("invokeRemote(" + channel + ", " + objectId + ", " + procId + ", "
+ callType + ", " + (args == null ? "null" : Arrays.asList(args)) + ")");
}
switch( callType ) {
case Asynchronous:
log.finest("Sending reliable asynchronous.");
rpc.callAsync(channel, objectId, procId, args);
return null;
case Unreliable:
log.finest("Sending unreliable asynchronous.");
rpc.callAsync((byte)MessageConnection.CHANNEL_DEFAULT_UNRELIABLE, objectId, procId, args);
return null;
default:
case Synchronous:
log.finest("Sending synchronous.");
Object result = rpc.callAndWait(channel, objectId, procId, args);
if( log.isLoggable(Level.FINEST) ) {
log.finest("->got:" + result);
}
return result;
}
}
/**
* Handle remote object registry updates from the other end.
*/
protected void rmiUpdate( short procId, Object[] args ) {
if( log.isLoggable(Level.FINEST) ) {
log.finest("rmiUpdate(" + procId + ", " + Arrays.asList(args) + ")");
}
switch( procId ) {
case NEW_CLASS:
addRemoteClass((ClassInfo)args[0]);
break;
case REMOVE_OBJECT:
removeRemoteObject((Short)args[0]);
break;
case ADD_OBJECT:
ClassInfo info = remote.classes.get((Short)args[3]);
addRemoteObject((Byte)args[0], (Short)args[1], (String)args[2], info);
break;
}
}
/**
* Handle the actual remote object method calls.
*/
protected Object invokeLocal( short objectId, short procId, Object[] args ) {
// Actually could use a regular concurrent map for this
// Only lock the local registry during lookup and
// not invocation. It prevents a deadlock if the invoked method
// tries to share an object. It should be safe.
SharedObject share = local.byId.get(objectId);
local.lock.readLock().lock();
try {
share = local.byId.get(objectId);
} finally {
local.lock.readLock().unlock();
}
try {
RmiContext.setRmiConnection(context);
return share.invoke(procId, args);
} finally {
RmiContext.setRmiConnection(null);
}
}
private class SharedObject {
private final short objectId;
private final String name;
private final Object object;
private final Class type;
private final ClassInfo classInfo;
public SharedObject( String name, Object object, Class type, ClassInfo classInfo ) {
this.objectId = (short)nextObjectId.incrementAndGet();
this.name = name;
this.object = object;
this.type = type;
this.classInfo = classInfo;
}
public Object invoke( short procId, Object[] args ) {
return classInfo.getMethod(procId).invoke(object, args);
}
}
private class RmiHandler implements RpcHandler {
@Override
public Object call( RpcConnection conn, short objectId, short procId, Object... args ) {
if( objectId == rmiId ) {
rmiUpdate(procId, args);
return null;
} else {
return invokeLocal(objectId, procId, args);
}
}
}
/**
* Keeps a coincident index between short ID, name, and related class info.
* There will be one of these to track our local objects and one to track
* the remote objects and a lock that can guard them.
*/
private class ObjectIndex<T> {
final Map<String, T> byName = new HashMap<String, T>();
final Map<Short, T> byId = new HashMap<Short, T>();
final Map<Short, ClassInfo> classes = new HashMap<Short, ClassInfo>();
final ReadWriteLock lock = new ReentrantReadWriteLock();
public ObjectIndex() {
}
}
}

@ -39,6 +39,8 @@ import com.jme3.network.message.SerializerRegistrationsMessage;
import com.jme3.network.serializing.Serializer; import com.jme3.network.serializing.Serializer;
import com.jme3.network.service.AbstractClientService; import com.jme3.network.service.AbstractClientService;
import com.jme3.network.service.ClientServiceManager; import com.jme3.network.service.ClientServiceManager;
import java.util.logging.Level;
import java.util.logging.Logger;
/** /**
@ -48,19 +50,27 @@ import com.jme3.network.service.ClientServiceManager;
*/ */
public class ClientSerializerRegistrationsService extends AbstractClientService public class ClientSerializerRegistrationsService extends AbstractClientService
implements MessageListener<Client> { implements MessageListener<Client> {
static final Logger log = Logger.getLogger(SerializerRegistrationsMessage.class.getName());
@Override @Override
protected void onInitialize( ClientServiceManager serviceManager ) { protected void onInitialize( ClientServiceManager serviceManager ) {
// Make sure our message type is registered
// This is the minimum we'd need just to be able to register // Make sure our message type is registered if it isn't already
// the rest... otherwise we can't even receive this message. if( Serializer.getExactSerializerRegistration(SerializerRegistrationsMessage.class) == null ) {
Serializer.registerClass(SerializerRegistrationsMessage.class); // This is the minimum we'd need just to be able to register
Serializer.registerClass(SerializerRegistrationsMessage.Registration.class); // the rest... otherwise we can't even receive this message.
Serializer.registerClass(SerializerRegistrationsMessage.class);
Serializer.registerClass(SerializerRegistrationsMessage.Registration.class);
} else {
log.log(Level.INFO, "Skipping registration of SerializerRegistrationsMessage.");
}
// Add our listener for that message type // Add our listener for that message type
serviceManager.getClient().addMessageListener(this, SerializerRegistrationsMessage.class); serviceManager.getClient().addMessageListener(this, SerializerRegistrationsMessage.class);
} }
@Override
public void messageReceived( Client source, Message m ) { public void messageReceived( Client source, Message m ) {
// We only wait for one kind of message... // We only wait for one kind of message...
SerializerRegistrationsMessage msg = (SerializerRegistrationsMessage)m; SerializerRegistrationsMessage msg = (SerializerRegistrationsMessage)m;

Loading…
Cancel
Save