Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ public void run()

Message m = null;
while( (m = messageBuffer.pollMessage()) != null ) {
m.setReliable( reliable );
m = m.setReliable( reliable );
dispatch( m );
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ protected void createAndDispatch( Envelope env )
// Should be complete... and maybe we should check, but we don't.
Message m = null;
while( (m = protocol.pollMessage()) != null ) {
m.setReliable(reliable);
m = m.setReliable(reliable);
dispatch(env.getSource(), m);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.function.Predicate;
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
Expand All @@ -68,6 +70,17 @@ public abstract class Serializer {

private static final Serializer fieldSerializer = new FieldSerializer();
private static final Serializer arraySerializer = new ArraySerializer();

// Predicate-based registry for dynamic serializer resolution
private static class PredicateRegistration {
final Predicate<Class<?>> predicate;
final Serializer serializer;
PredicateRegistration(Predicate<Class<?>> p, Serializer s) {
this.predicate = p;
this.serializer = s;
}
}
private static final List<PredicateRegistration> predicateSerializers = new ArrayList<>();
Comment thread
thkluge marked this conversation as resolved.
Resolved

private static short nextAvailableId = -2; // historically the first ID was always -2

Expand Down Expand Up @@ -95,7 +108,8 @@ public static void initialize() {
// Reset all of the indices and tracking variables just in case
idRegistrations.clear();
classRegistrations.clear();
registrations.clear();
registrations.clear();
predicateSerializers.clear();

nextAvailableId = -2; // historically the first ID was always -2

Expand Down Expand Up @@ -278,17 +292,37 @@ public static SerializerRegistration registerClass(Class cls, boolean failOnMiss
short classId = serializable.id();
if (classId == 0) classId = nextId();

Serializer serializer = getSerializer(serializerClass, false);
Serializer serializer = null;

if (serializer == null) serializer = fieldSerializer;
// ignore both FieldSerializer and Serializer defaults.
if (serializerClass != null && serializerClass != FieldSerializer.class && serializerClass != Serializer.class) {
try {
// Directly instantiate the custom serializer
serializer = (Serializer) serializerClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
log.log(Level.SEVERE, "Could not instantiate explicit custom serializer: " + serializerClass, e);
}
}

// no explicit custom serializer was requested
if (serializer == null) {
for (PredicateRegistration pr : predicateSerializers) {
if (pr.predicate.test(cls)) {
serializer = pr.serializer;
break;
}
}
}

SerializerRegistration existingReg = getExactSerializerRegistration(cls);
// Fallback to standard resolution if no predicate matched
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 );
return registerClassForId(classId, cls, serializer);
}
if (failOnMiss) {
Comment thread
thkluge marked this conversation as resolved.
Resolved
throw new IllegalArgumentException( "Class is not marked @Serializable:" + cls );
Expand Down Expand Up @@ -465,7 +499,20 @@ public static SerializerRegistration getSerializerRegistration(Class cls, boolea
if( failOnMiss ) {
throw new IllegalArgumentException( "Class has not been registered:" + cls );
}
return registerClass(cls, fieldSerializer);

// check predicates for auto-registration fallback
Serializer fallbackSerializer = null;
for (PredicateRegistration pr : predicateSerializers) {
if (pr.predicate.test(cls)) {
fallbackSerializer = pr.serializer;
break;
}
}
if (fallbackSerializer == null) {
fallbackSerializer = fieldSerializer;
}

return registerClass(cls, fallbackSerializer);
}


Expand Down Expand Up @@ -551,6 +598,24 @@ public static void writeClassAndObject(ByteBuffer buffer, Object object) throws

reg.getSerializer().writeObject(buffer, object);
}

/**
* Registers a predicate-based dynamic serializer.
* <p>
* <b>Important Ordering Behavior:</b> Predicates are evaluated in registration order
* using a <b>first-match-wins</b> strategy. Furthermore, predicates only affect classes
* registered <i>afterwards</i>; any classes already registered prior to this call will
* not be re-evaluated against the predicate.
*
* @param predicate the condition used to match classes dynamically
* @param serializer the serializer instance to assign when the predicate matches
*/
public static void registerSerializerPredicate(Predicate<Class<?>> predicate, Serializer serializer) {
if (locked) {
throw new RuntimeException("Serializer registry locked trying to register predicate");
}
predicateSerializers.add(new PredicateRegistration(predicate, serializer));
}

/**
* Read an object from the buffer, effectively deserializing it.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/*
* Copyright (c) 2009-2026 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.network.serializing.serializers.FieldSerializer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.nio.ByteBuffer;

import static org.junit.jupiter.api.Assertions.*;

public class PredicateSerializerTest {

// A dummy message representing a custom data structure (e.g., mimicking a Record or Kotlin Data Class)
@Serializable
public static class SpecialMessage {
public String data;
public SpecialMessage() {}
}

// A standard message that should fall back to the default FieldSerializer
@Serializable
public static class StandardMessage {
public int id;
public StandardMessage() {}
}

@Serializable(serializer = Serializer.class)
public static class SpecialMessageWithAlternateDefault {
public SpecialMessageWithAlternateDefault() {}
}

// Simulates a class where the user EXPLICITLY requested a custom serializer
@Serializable(serializer = SecondMockSerializer.class)
public static class ExplicitCustomMessage {
public ExplicitCustomMessage() {}
}

public static class SecondMockSerializer extends Serializer {
@Override
public <T> T readObject(ByteBuffer data, Class<T> c) { return null; }
@Override
public void writeObject(ByteBuffer buffer, Object object) {}
}

// A mock custom serializer to test the routing mechanism
public static class MockCustomSerializer extends Serializer {
@Override
public <T> T readObject(ByteBuffer data, Class<T> c) {
return null; // Mock implementation
}

@Override
public void writeObject(ByteBuffer buffer, Object object) {
// Mock implementation
}
}

@BeforeEach
public void setupRegistry() {
Serializer.initialize();

// Register the predicate: any class matching SpecialMessage gets the MockCustomSerializer
Serializer.registerSerializerPredicate(
cls -> cls == SpecialMessage.class,
new MockCustomSerializer()
);
}

@Test
public void testPredicateSerializerSelectionOverridesDefault() {
// Register the class
Serializer.registerClass(SpecialMessage.class);

SerializerRegistration registration = Serializer.getExactSerializerRegistration(SpecialMessage.class);

assertNotNull(registration, "Registration should not be null");
// Verify the predicate successfully intercepted the registration and applied the mock serializer
assertEquals(MockCustomSerializer.class, registration.getSerializer().getClass(),
"SpecialMessage should be routed to MockCustomSerializer via predicate");
}

@Test
public void testStandardClassFallsBackToFieldSerializer() {
// Register a normal POJO
Serializer.registerClass(StandardMessage.class);

SerializerRegistration registration = Serializer.getExactSerializerRegistration(StandardMessage.class);

assertNotNull(registration, "Registration should not be null");
// Verify classes that do not match the predicate still receive the default FieldSerializer
assertEquals(FieldSerializer.class, registration.getSerializer().getClass(),
"StandardMessage should fall back to the default FieldSerializer");
}

@Test
public void testPredicateAutoRegistrationFallback() {
// Clear registrations
Serializer.initialize();
Serializer.registerSerializerPredicate(cls -> cls == SpecialMessage.class, new MockCustomSerializer());

// Fetch the registration using the auto-registration fallback mechanism (passing failOnMiss=false implicitly or explicitly)
SerializerRegistration registration = Serializer.getSerializerRegistration(SpecialMessage.class, false);

assertNotNull(registration, "Auto-registration should successfully create a registration");
assertEquals(MockCustomSerializer.class, registration.getSerializer().getClass(),
"Auto-registration should respect the registered predicates");
}

@Test
public void testInitializeClearsPredicateRegistrations() {
// @BeforeEach already called initialize() and registered MockCustomSerializer for SpecialMessage.

// Re-initialize the registry. This MUST clear the predicateSerializers list.
Serializer.initialize();

// Register the exact same predicate condition, but with a different serializer (SecondMockSerializer)
Serializer.registerSerializerPredicate(
cls -> cls == SpecialMessage.class,
new SecondMockSerializer()
);

// Register the class
Serializer.registerClass(SpecialMessage.class);

SerializerRegistration registration = Serializer.getExactSerializerRegistration(SpecialMessage.class);

assertNotNull(registration, "Registration should not be null");

// If initialize() failed to clear the list, MockCustomSerializer would still be first and win.
// If it correctly cleared the list, SecondMockSerializer should be assigned.
assertEquals(SecondMockSerializer.class, registration.getSerializer().getClass(),
"initialize() failed to clear predicateSerializers, causing a stale serializer to be selected.");
}

@Test
public void testPredicateEvaluatesForAlternateDefaultSerializer() {
Serializer.initialize();
Serializer.registerSerializerPredicate(
cls -> cls == SpecialMessageWithAlternateDefault.class,
new MockCustomSerializer()
);

Serializer.registerClass(SpecialMessageWithAlternateDefault.class);
SerializerRegistration registration = Serializer.getExactSerializerRegistration(SpecialMessageWithAlternateDefault.class);

assertNotNull(registration);

// FAILS HERE in the flawed implementation:
// The registry sees `Serializer.class`, skips the predicate loop,
// and falls back to `FieldSerializer`, ignoring your mock!
assertEquals(MockCustomSerializer.class, registration.getSerializer().getClass(),
"Predicate should intercept the alternate default (Serializer.class)");
}

@Test
public void testExplicitCustomSerializerOverridesPredicate() {
Serializer.initialize();
// Register a predicate that TRIES to intercept this class
Serializer.registerSerializerPredicate(
cls -> cls == ExplicitCustomMessage.class,
new MockCustomSerializer()
);

// Because the user explicitly annotated this with `@Serializable(serializer = SecondMockSerializer.class)`,
// the explicit request must win, and the predicate must be ignored.
Serializer.registerClass(ExplicitCustomMessage.class);
SerializerRegistration registration = Serializer.getExactSerializerRegistration(ExplicitCustomMessage.class);

assertNotNull(registration);
assertEquals(SecondMockSerializer.class, registration.getSerializer().getClass(),
"Explicit custom serializer must override predicate selection");
}
}