com.jme3.network.serializing.Serializer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jme3-networking Show documentation
Show all versions of jme3-networking Show documentation
jMonkeyEngine is a 3-D game engine for adventurous Java developers
/*
* Copyright (c) 2009-2021 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.serializing;
import com.jme3.math.Vector3f;
import com.jme3.network.message.ChannelInfoMessage;
import com.jme3.network.message.ClientRegistrationMessage;
import com.jme3.network.message.DisconnectMessage;
import com.jme3.network.message.GZIPCompressedMessage;
import com.jme3.network.message.ZIPCompressedMessage;
import com.jme3.network.serializing.serializers.*;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.jar.Attributes;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* The main serializer class, which will serialize objects such that
* they can be sent across the network. Serializing classes should extend
* this to provide their own serialization.
*
* @author Lars Wesselius
*/
public abstract class Serializer {
protected static final Logger log = Logger.getLogger(Serializer.class.getName());
private static final SerializerRegistration NULL_CLASS = new SerializerRegistration( null, Void.class, (short)-1 );
private static final Map idRegistrations = new HashMap();
private static final Map classRegistrations = new HashMap();
private static final List registrations = new ArrayList();
private static final Serializer fieldSerializer = new FieldSerializer();
private static final Serializer arraySerializer = new ArraySerializer();
private static short nextAvailableId = -2; // historically the first ID was always -2
private static boolean strictRegistration = true;
private static volatile boolean locked = false;
// Registers the classes we already have serializers for.
static {
initialize();
}
public static void initialize() {
// Reset all of the indices and tracking variables just in case
idRegistrations.clear();
classRegistrations.clear();
registrations.clear();
nextAvailableId = -2; // historically the first ID was always -2
// Obviously need to be unlocked...
locked = false;
// Preregister some fixed serializers so that they don't move
// if the list below is modified. Automatic ID generation will
// skip these IDs.
registerClassForId( DisconnectMessage.SERIALIZER_ID, DisconnectMessage.class,
new DisconnectMessage.DisconnectSerializer() );
registerClassForId( ClientRegistrationMessage.SERIALIZER_ID, ClientRegistrationMessage.class,
new ClientRegistrationMessage.ClientRegistrationSerializer() );
registerClass(boolean.class, new BooleanSerializer());
registerClass(byte.class, new ByteSerializer());
registerClass(char.class, new CharSerializer());
registerClass(short.class, new ShortSerializer());
registerClass(int.class, new IntSerializer());
registerClass(long.class, new LongSerializer());
registerClass(float.class, new FloatSerializer());
registerClass(double.class, new DoubleSerializer());
registerClass(Boolean.class, new BooleanSerializer());
registerClass(Byte.class, new ByteSerializer());
registerClass(Character.class, new CharSerializer());
registerClass(Short.class, new ShortSerializer());
registerClass(Integer.class, new IntSerializer());
registerClass(Long.class, new LongSerializer());
registerClass(Float.class, new FloatSerializer());
registerClass(Double.class, new DoubleSerializer());
registerClass(String.class, new StringSerializer());
registerClass(Vector3f.class, new Vector3Serializer());
registerClass(Date.class, new DateSerializer());
// all the Collection classes go here
registerClass(AbstractCollection.class, new CollectionSerializer());
registerClass(AbstractList.class, new CollectionSerializer());
registerClass(AbstractSet.class, new CollectionSerializer());
registerClass(ArrayList.class, new CollectionSerializer());
registerClass(HashSet.class, new CollectionSerializer());
registerClass(LinkedHashSet.class, new CollectionSerializer());
registerClass(LinkedList.class, new CollectionSerializer());
registerClass(TreeSet.class, new CollectionSerializer());
registerClass(Vector.class, new CollectionSerializer());
// All the Map classes go here
registerClass(AbstractMap.class, new MapSerializer());
registerClass(Attributes.class, new MapSerializer());
registerClass(HashMap.class, new MapSerializer());
registerClass(Hashtable.class, new MapSerializer());
registerClass(IdentityHashMap.class, new MapSerializer());
registerClass(TreeMap.class, new MapSerializer());
registerClass(WeakHashMap.class, new MapSerializer());
registerClass(Enum.class, new EnumSerializer());
registerClass(GZIPCompressedMessage.class, new GZIPSerializer());
registerClass(ZIPCompressedMessage.class, new ZIPSerializer());
registerClass(ChannelInfoMessage.class);
}
/**
* When set to true, classes that do not have intrinsic IDs in their
* {@code @Serializable } will not be auto-registered during write. Defaults
* to true since this is almost never desired behavior with the way
* this code works. Set to false to get the old permissive behavior.
*/
public static void setStrictRegistration( boolean b ) {
strictRegistration = b;
}
public static SerializerRegistration registerClass(Class cls) {
return registerClass(cls, true);
}
public static void registerClasses(Class... classes) {
for( Class c : classes ) {
registerClass(c);
}
}
private static short nextId() {
// If the ID we are about to return is already in use
// then skip it.
while (idRegistrations.containsKey(nextAvailableId) ) {
nextAvailableId--;
}
// Return the available ID and post-decrement to get
// ready for next time.
return nextAvailableId--;
}
/**
* Can put the registry in a read-only state such that additional attempts
* to register classes will fail. This can be used by servers to lock the
* registry to avoid accidentally registering classes after a full registry
* set has been compiled.
*/
public static void setReadOnly( boolean b ) {
locked = b;
}
public static boolean isReadOnly() {
return locked;
}
/**
* Directly registers a class for a specific ID. Generally, use the regular
* registerClass() method. This method is intended for framework code that might
* be maintaining specific ID maps across client and server.
*/
public static SerializerRegistration registerClassForId( short id, Class cls, Serializer serializer ) {
if( locked ) {
throw new RuntimeException("Serializer registry locked trying to register class:" + cls);
}
SerializerRegistration reg = new SerializerRegistration(serializer, cls, id);
idRegistrations.put(id, reg);
classRegistrations.put(cls, reg);
log.log( Level.FINE, "Registered class[" + id + "]:{0} to:" + serializer, cls );
serializer.initialize(cls);
// Add the class after so that dependency order is preserved if the
// serializer registers its own classes.
registrations.add(reg);
return reg;
}
/**
* Registers the specified class. The failOnMiss flag controls whether
* this method returns null for failed registration or throws an exception.
*/
@SuppressWarnings("unchecked")
public static SerializerRegistration registerClass(Class cls, boolean failOnMiss) {
if (cls.isAnnotationPresent(Serializable.class)) {
Serializable serializable = (Serializable)cls.getAnnotation(Serializable.class);
Class serializerClass = serializable.serializer();
short classId = serializable.id();
if (classId == 0) classId = nextId();
Serializer serializer = getSerializer(serializerClass, false);
if (serializer == null) serializer = fieldSerializer;
SerializerRegistration existingReg = getExactSerializerRegistration(cls);
if (existingReg != null) classId = existingReg.getId();
SerializerRegistration reg = new SerializerRegistration(serializer, cls, classId);
return registerClassForId( classId, cls, serializer );
}
if (failOnMiss) {
throw new IllegalArgumentException( "Class is not marked @Serializable:" + cls );
}
return null;
}
/**
* @deprecated This cannot be implemented in a reasonable way that works in
* all deployment methods.
*/
@Deprecated
public static SerializerRegistration[] registerPackage(String pkgName) {
try {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
String path = pkgName.replace('.', '/');
Enumeration resources = classLoader.getResources(path);
List dirs = new ArrayList<>();
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
dirs.add(new File(resource.getFile()));
}
ArrayList classes = new ArrayList<>();
for (File directory : dirs) {
classes.addAll(findClasses(directory, pkgName));
}
SerializerRegistration[] registeredClasses = new SerializerRegistration[classes.size()];
for (int i = 0; i != classes.size(); ++i) {
Class clz = classes.get(i);
registeredClasses[i] = registerClass(clz, false);
}
return registeredClasses;
} catch (Exception e) {
e.printStackTrace();
}
return new SerializerRegistration[0];
}
private static List findClasses(File dir, String pkgName) throws ClassNotFoundException {
List classes = new ArrayList<>();
if (!dir.exists()) {
return classes;
}
File[] files = dir.listFiles();
for (File file : files) {
if (file.isDirectory()) {
assert !file.getName().contains(".");
classes.addAll(findClasses(file, pkgName + "." + file.getName()));
} else if (file.getName().endsWith(".class")) {
classes.add(Class.forName(pkgName + '.' + file.getName().substring(0, file.getName().length() - 6)));
}
}
return classes;
}
public static SerializerRegistration registerClass(Class cls, Serializer serializer) {
SerializerRegistration existingReg = getExactSerializerRegistration(cls);
short id;
if (existingReg != null) {
id = existingReg.getId();
} else {
id = nextId();
}
return registerClassForId( id, cls, serializer );
}
public static Serializer getExactSerializer(Class cls) {
return classRegistrations.get(cls).getSerializer();
}
public static Serializer getSerializer(Class cls) {
return getSerializer(cls, true);
}
public static Serializer getSerializer(Class cls, boolean failOnMiss) {
return getSerializerRegistration(cls, failOnMiss).getSerializer();
}
public static Collection getSerializerRegistrations() {
return registrations;
}
public static SerializerRegistration getExactSerializerRegistration(Class cls) {
return classRegistrations.get(cls);
}
public static SerializerRegistration getSerializerRegistration(Class cls) {
return getSerializerRegistration(cls, strictRegistration);
}
@SuppressWarnings("unchecked")
public static SerializerRegistration getSerializerRegistration(Class cls, boolean failOnMiss) {
SerializerRegistration reg = classRegistrations.get(cls);
if (reg != null) return reg;
for (Map.Entry entry : classRegistrations.entrySet()) {
if (entry.getKey().isAssignableFrom(Serializable.class)) continue;
if (entry.getKey().isAssignableFrom(cls)) return entry.getValue();
}
if (cls.isArray()) return registerClass(cls, arraySerializer);
if (Serializable.class.isAssignableFrom(cls)) {
return getExactSerializerRegistration(java.io.Serializable.class);
}
// See if the class could be safely auto-registered
if (cls.isAnnotationPresent(Serializable.class)) {
Serializable serializable = (Serializable)cls.getAnnotation(Serializable.class);
short classId = serializable.id();
if( classId != 0 ) {
// No reason to fail because the ID is fixed
failOnMiss = false;
}
}
if( failOnMiss ) {
throw new IllegalArgumentException( "Class has not been registered:" + cls );
}
return registerClass(cls, fieldSerializer);
}
///////////////////////////////////////////////////////////////////////////////////
/**
* Read the class from given buffer and return its SerializerRegistration.
*
* @param buffer The buffer to read from.
* @return The SerializerRegistration, or null if non-existent.
*/
public static SerializerRegistration readClass(ByteBuffer buffer) {
short classID = buffer.getShort();
if (classID == -1) return NULL_CLASS;
return idRegistrations.get(classID);
}
/**
* Read the class and the object.
*
* @param buffer Buffer to read from.
* @return The Object that was read.
* @throws IOException If serialization failed.
*/
@SuppressWarnings("unchecked")
public static Object readClassAndObject(ByteBuffer buffer) throws IOException {
SerializerRegistration reg = readClass(buffer);
if (reg == NULL_CLASS) return null;
if (reg == null) throw new SerializerException( "Class not found for buffer data." );
return reg.getSerializer().readObject(buffer, reg.getType());
}
/**
* Write a class and return its SerializerRegistration.
*
* @param buffer The buffer to write the given class to.
* @param type The class to write.
* @return The SerializerRegistration that's registered to the class.
*/
public static SerializerRegistration writeClass(ByteBuffer buffer, Class type) throws IOException {
SerializerRegistration reg = getSerializerRegistration(type);
if (reg == null) {
throw new SerializerException( "Class not registered:" + type );
}
if( log.isLoggable(Level.FINER) ) {
log.log(Level.FINER, "writing class:{0} with ID:{1}", new Object[]{type, reg.getId()});
}
buffer.putShort(reg.getId());
return reg;
}
/**
* Write the class and object.
*
* @param buffer The buffer to write to.
* @param object The object to write.
* @throws IOException If serializing fails.
*/
public static void writeClassAndObject(ByteBuffer buffer, Object object) throws IOException {
if (object == null) {
buffer.putShort((short)-1);
return;
}
SerializerRegistration reg = writeClass(buffer, object.getClass());
// If the caller (or us) has registered a generic base class (like Enum)
// that is meant to steer automatic resolution for things like FieldSerializer
// that have final classes in fields... then there are cases where the exact
// type isn't known by the outer class. (Think of a message object
// that has an Object field but tries to send an Enum subclass in it.)
// In that case, the SerializerRegistration object we get back isn't
// really going to be capable of recreating the object on the other
// end because it won't know what class to use. This only comes up
// in writeClassAndObject() because we just wrote an ID to a more generic
// class than will be readable on the other end. The check is simple, though.
if( reg.getType() != object.getClass() ) {
throw new IllegalArgumentException("Class has not been registered:"
+ object.getClass() + " but resolved to generic serializer for:" + reg.getType());
}
reg.getSerializer().writeObject(buffer, object);
}
/**
* Read an object from the buffer, effectively deserializing it.
*
* @param data The buffer to read from.
* @param c The class of the object.
* @return The object read.
* @throws IOException If deserializing fails.
*/
public abstract T readObject(ByteBuffer data, Class c) throws IOException;
/**
* Write an object to the buffer, effectively serializing it.
*
* @param buffer The buffer to write to.
* @param object The object to serialize.
* @throws IOException If serializing fails.
*/
public abstract void writeObject(ByteBuffer buffer, Object object) throws IOException;
/**
* Registration for when a serializer may need to cache something.
*
* Override to use.
*
* @param clazz The class that has been registered to the serializer.
*/
public void initialize(Class clazz) { }
}