diff --git a/pom.xml b/pom.xml index 49a1f6e00..995a56233 100644 --- a/pom.xml +++ b/pom.xml @@ -188,7 +188,7 @@ 2.26ea0 https://teamcity.chronicle.software/repository/download - 98.4 + 98.3 skip-internal-packages diff --git a/src/main/java/net/openhft/chronicle/wire/GenerateMethodReader.java b/src/main/java/net/openhft/chronicle/wire/GenerateMethodReader.java index 656637626..e2fef57e8 100644 --- a/src/main/java/net/openhft/chronicle/wire/GenerateMethodReader.java +++ b/src/main/java/net/openhft/chronicle/wire/GenerateMethodReader.java @@ -48,7 +48,7 @@ */ public class GenerateMethodReader { - // Configuration flag for dumping the generated code. + // Configuration flag for dumping the generated code. private static final boolean DUMP_CODE = Jvm.getBoolean("dumpCode"); // Set of interfaces that are not meant to be processed. private static final Set> IGNORED_INTERFACES = new LinkedHashSet<>(); @@ -78,7 +78,10 @@ public class GenerateMethodReader { // Configuration for the type of wire to use for serialization/deserialization. private final WireType wireType; - // Handlers for metadata during the method reader generation. + // Check for supporting parameters which can either be non-Marshallable or Marshallable + private final Boolean multipleNonMarshallableParamTypes; + + // Handlers for metadata during the method reader generation. private final Object[] metaDataHandler; // Instances of the classes/interfaces for which method readers are to be generated. @@ -118,14 +121,15 @@ public class GenerateMethodReader { * Constructs a new instance of GenerateMethodReader. * Initializes the required configurations, metadata handlers, and instances which are essential for code generation. * - * @param wireType Configuration for serialization/deserialization - * @param interceptor An instance of MethodReaderInterceptorReturns + * @param wireType Configuration for serialization/deserialization + * @param interceptor An instance of MethodReaderInterceptorReturns * @param metaDataHandler Array of meta-data handlers - * @param instances Instances that dictate the structure of the generated MethodReader + * @param instances Instances that dictate the structure of the generated MethodReader */ - public GenerateMethodReader(WireType wireType, MethodReaderInterceptorReturns interceptor, Object[] metaDataHandler, Object... instances) { + public GenerateMethodReader(WireType wireType, MethodReaderInterceptorReturns interceptor, Boolean multipleNonMarshallableParamTypes, Object[] metaDataHandler, Object... instances) { this.wireType = wireType; this.interceptor = interceptor; + this.multipleNonMarshallableParamTypes = multipleNonMarshallableParamTypes; this.metaDataHandler = metaDataHandler; this.instances = instances; this.generatedClassName = generatedClassName0(); @@ -135,7 +139,7 @@ public GenerateMethodReader(WireType wireType, MethodReaderInterceptorReturns in * Computes the signature of a given method. * The signature comprises the return type, method name, and parameter types. * - * @param m The method for which the signature is to be computed + * @param m The method for which the signature is to be computed * @param type The type under consideration * @return A string representing the method's signature */ @@ -239,6 +243,7 @@ private void generateSourceCode() { sourceCode.append(format("package %s;\n", packageName())); // Import statements required for the generated code. + boolean hasMultipleNonMarshallableParamTypes = !Boolean.FALSE.equals(multipleNonMarshallableParamTypes); sourceCode.append("" + "import net.openhft.chronicle.core.Jvm;\n" + "import net.openhft.chronicle.core.util.InvocationTargetRuntimeException;\n" + @@ -248,7 +253,9 @@ private void generateSourceCode() { "import net.openhft.chronicle.wire.utils.*;\n" + "import net.openhft.chronicle.wire.BinaryWireCode;\n" + "\n" + + (hasMultipleNonMarshallableParamTypes ? "import java.util.HashMap;\n" : "") + "import java.util.Map;\n" + + (hasMultipleNonMarshallableParamTypes ? "import java.util.function.Function;\n" : "") + "import java.lang.reflect.Method;\n" + "\n"); @@ -462,9 +469,9 @@ private void generateSourceCode() { * @param anInterface The interface being processed. * @param instanceFieldName In the generated code, methods are executed on a field with this name. * @param methodFilter Indicates if the passed interface is marked with {@link MethodFilterOnFirstArg}. If true, only certain methods are processed. - * @ blocks based on method event IDs. * @param eventNameSwitchBlock The block of code that handles the switching of event names. * @param eventIdSwitchBlock The block of code that handles the switching of event IDs. + * @ blocks based on method event IDs. */ private void handleInterface(Class anInterface, String instanceFieldName, boolean methodFilter, SourceCodeFormatter eventNameSwitchBlock, SourceCodeFormatter eventIdSwitchBlock) { if (Jvm.dontChain(anInterface)) @@ -525,11 +532,11 @@ private void handleInterface(Class anInterface, String instanceFieldName, boo * *

Finally, if the method's return type is chainable, it calls {@code handleInterface()} on it. * - * @param m The method for which code is generated. - * @param anInterface The interface containing the method. - * @param instanceFieldName In the generated code, this method is executed on a field with this name. - * @param methodFilter Indicates if the passed interface is marked with {@link MethodFilterOnFirstArg}. If true, only certain methods are processed. - * @param eventIdSwitchBlock The block of code that handles the switching of event IDs. + * @param m The method for which code is generated. + * @param anInterface The interface containing the method. + * @param instanceFieldName In the generated code, this method is executed on a field with this name. + * @param methodFilter Indicates if the passed interface is marked with {@link MethodFilterOnFirstArg}. If true, only certain methods are processed. + * @param eventIdSwitchBlock The block of code that handles the switching of event IDs. * @param eventNameSwitchBlock The block of code that handles the switching of event names. */ private void handleMethod(Method m, Class anInterface, String instanceFieldName, boolean methodFilter, SourceCodeFormatter eventNameSwitchBlock, SourceCodeFormatter eventIdSwitchBlock) { @@ -555,10 +562,15 @@ private void handleMethod(Method m, Class anInterface, String instanceFieldNa final String typeName = parameterType.getCanonicalName(); String fieldName = m.getName() + "arg" + i; if (fieldNames.add(fieldName)) { - if (parameterType == Bytes.class) + if (parameterType == Bytes.class) { fields.append(format("private Bytes %s = Bytes.allocateElasticOnHeap();\n", fieldName)); - else + } else { + if (!parameterType.isPrimitive() && !Modifier.isFinal(parameterType.getModifiers()) && multipleNonMarshallableParamTypes(parameterType)) { + fields.append(format("private final Map, %s> %sInstances = new HashMap<>();\n", typeName, typeName, fieldName)); + fields.append(format("private final Function, %s> %sFunc = %sInstances::get;\n", typeName, typeName, fieldName, fieldName)); + } fields.append(format("private %s %s;\n", typeName, fieldName)); + } } } @@ -756,13 +768,12 @@ private String methodCall(Method m, String instanceFieldName, String chainedCall * influence the generated code. If {@link LongConversion} * annotations are present on the argument, a converter field is registered. * - * @param m Method for which an argument is read. - * @param argIndex Index of an argument to be read. - * @param inLambda {@code true} if argument is read in a lambda passed to a - * {@link ValueIn#sequence(Object, BiConsumer)} call. + * @param m Method for which an argument is read. + * @param argIndex Index of an argument to be read. + * @param inLambda {@code true} if argument is read in a lambda passed to a + * {@link ValueIn#sequence(Object, BiConsumer)} call. * @param parameterTypes The types of the method parameters. * @return Code in the form of a String that retrieves the specified argument from {@link ValueIn} input. - * * @see LongConversion * @see ValueIn */ @@ -870,13 +881,28 @@ else if (numericConversionClass != null && LongConverter.class.isAssignableFrom( } else { // Handling other object types. final String typeName = argumentType.getCanonicalName(); - if (!argumentType.isArray() && !AbstractMarshallableCfg.class.isAssignableFrom(argumentType) && !Collection.class.isAssignableFrom(argumentType) && !Map.class.isAssignableFrom(argumentType) && Object.class != argumentType && !argumentType.isInterface()) { - return format("%s = %s.object(%s, %s.class);\n", argumentName, valueInName, argumentName, typeName); + boolean multipleNonMarshallableParamTypes = multipleNonMarshallableParamTypes(argumentType); + if (!Modifier.isFinal(argumentType.getModifiers()) && multipleNonMarshallableParamTypes) { + return format("%s = %s.object(%s, %s.class); %sInstances.put(%s.getClass(), %s);\n", argumentName, valueInName, argumentName + "Func", typeName, argumentName, argumentName, argumentName); + } + if (isRecyclable(argumentType)) { + return format("%s = %s.object(checkRecycle(%s), %s.class);\n", argumentName, valueInName, argumentName, typeName); } - return format("%s = %s.object(checkRecycle(%s), %s.class);\n", argumentName, valueInName, argumentName, typeName); + return format("%s = %s.object(%s, %s.class);\n", argumentName, valueInName, argumentName, typeName); } } + private static boolean isRecyclable(Class argumentType) { + return argumentType.isArray() || AbstractMarshallableCfg.class.isAssignableFrom(argumentType) || Collection.class.isAssignableFrom(argumentType) || Map.class.isAssignableFrom(argumentType); + } + + private boolean multipleNonMarshallableParamTypes(Class argumentType) { + Boolean _multipleNonMarshallableParamTypes = this.multipleNonMarshallableParamTypes; + return _multipleNonMarshallableParamTypes == null + ? argumentType.isInterface() && !isRecyclable(argumentType) || argumentType == Object.class + : _multipleNonMarshallableParamTypes; + } + /** * Checks if a real interceptor is present that returns. * @@ -910,7 +936,8 @@ public String generatedClassName() { /** * Constructs the generated class name using various components such as - * the names of instances, metadata handlers, wire type, and potential interceptor. + * the names of instances, metadata handlers, wire type, support for interchangeable marshallable/non-marshallable + * ([T]rue, [F]alse or [A]uto) and potential interceptor. * Special characters, such as underscores and slashes, are handled to format the class name. * * @return The constructed name for the generated class. @@ -933,6 +960,11 @@ private String generatedClassName0() { sb.append(wireType.toString() .replace("_", "")); + // Append multi marshal/non-marshal support + if (multipleNonMarshallableParamTypes != null) { + sb.append(Boolean.TRUE.equals(multipleNonMarshallableParamTypes) ? 'T' : 'F'); + } + // Append interceptor details to the class name. if (interceptor instanceof GeneratingMethodReaderInterceptorReturns) sb.append(((GeneratingMethodReaderInterceptorReturns) interceptor).generatorId()); diff --git a/src/main/java/net/openhft/chronicle/wire/ValueIn.java b/src/main/java/net/openhft/chronicle/wire/ValueIn.java index 99151ae63..f8148995a 100644 --- a/src/main/java/net/openhft/chronicle/wire/ValueIn.java +++ b/src/main/java/net/openhft/chronicle/wire/ValueIn.java @@ -1267,6 +1267,27 @@ default E object(@Nullable E using, @Nullable Class clazz) thro return ValidatableUtil.validate(t); } + /** + * Reads an object from the wire. + * + * @param The type of the object to read. + * @param usingFunction A function to apply retrieve the instance of an object to reuse, or null to create a new instance. + * @param clazz The class of the object to read. + * @return The object read from the wire, or null if it cannot be read. + * @throws InvalidMarshallableException if the object is invalid + */ + @Nullable + default E object(@Nullable Function, E> usingFunction, @Nullable Class clazz) throws InvalidMarshallableException { + E t; + Object o = typePrefixOrObject(clazz); + if (o != null && !(o instanceof Class)) { + t = (@Nullable E) marshallable(o, MARSHALLABLE); + } else { + t = Wires.object2(this, o != null ? usingFunction.apply((Class) o) : null , clazz, true, (Class) o); + } + return ValidatableUtil.validate(t); + } + /** * Reads an object from the wire. * diff --git a/src/main/java/net/openhft/chronicle/wire/VanillaMethodReaderBuilder.java b/src/main/java/net/openhft/chronicle/wire/VanillaMethodReaderBuilder.java index 57a7c1060..fb3b57488 100644 --- a/src/main/java/net/openhft/chronicle/wire/VanillaMethodReaderBuilder.java +++ b/src/main/java/net/openhft/chronicle/wire/VanillaMethodReaderBuilder.java @@ -75,6 +75,10 @@ public class VanillaMethodReaderBuilder implements MethodReaderBuilder { // A flag to indicate whether the reader is in a scanning mode. private boolean scanning = false; + // A flag to determine support for parameters which can either be non-Marshallable or Marshallable + // null for auto-detect + private Boolean multipleNonMarshallableParamTypes = null; + /** * Constructs a new {@code VanillaMethodReaderBuilder} with the specified wire input. * @@ -189,6 +193,17 @@ public VanillaMethodReaderBuilder scanning(boolean scanning) { return this; } + /** + * Configures the reader to handle parameters which can have multiple non-marshallable parameter types. + * + * @param multipleNonMarshallableParamTypes Whether the reader should handle multiple non-marshallable parameter types. + * @return This builder instance for chaining. + */ + public VanillaMethodReaderBuilder multipleNonMarshallableParamTypes(Boolean multipleNonMarshallableParamTypes) { + this.multipleNonMarshallableParamTypes = multipleNonMarshallableParamTypes; + return this; + } + /** * Creates an instance of a generated method reader. * The method first checks if the desired generated reader class is already loaded. @@ -202,7 +217,7 @@ private MethodReader createGeneratedInstance(Object... impls) { if (ignoreDefaults || Jvm.getBoolean(DISABLE_READER_PROXY_CODEGEN)) return null; - GenerateMethodReader generateMethodReader = new GenerateMethodReader(wireType, methodReaderInterceptorReturns, metaDataHandler, impls); + GenerateMethodReader generateMethodReader = new GenerateMethodReader(wireType, methodReaderInterceptorReturns, multipleNonMarshallableParamTypes, metaDataHandler, impls); String fullClassName = generateMethodReader.packageName() + "." + generateMethodReader.generatedClassName(); diff --git a/src/test/java/net/openhft/chronicle/wire/MethodWriterVagueTypesTest.java b/src/test/java/net/openhft/chronicle/wire/MethodWriterVagueTypesTest.java new file mode 100644 index 000000000..9965e5e3f --- /dev/null +++ b/src/test/java/net/openhft/chronicle/wire/MethodWriterVagueTypesTest.java @@ -0,0 +1,228 @@ +package net.openhft.chronicle.wire; + +import net.openhft.chronicle.bytes.Bytes; +import net.openhft.chronicle.bytes.MethodReader; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; + +/** + * This class tests the behavior of MethodWriter when handling vague/interfaced messages. + * It extends the WireTestCommon from the `net.openhft.chronicle.wire` package for common test setup and utilities. + */ +@RunWith(value = Parameterized.class) +public class MethodWriterVagueTypesTest extends net.openhft.chronicle.wire.WireTestCommon { + private ArrayBlockingQueue singleQ = new ArrayBlockingQueue<>(1); + private ArrayBlockingQueue doubleQ = new ArrayBlockingQueue<>(2); + private final List, Object>> usedObjects = Arrays.asList(new HashMap<>(), new HashMap<>()); + private Class[] prevObjClasses = new Class[2]; + private final Boolean multipleNonMarshallableParamTypes; + + public MethodWriterVagueTypesTest(Boolean multipleNonMarshallableParamTypes) { + this.multipleNonMarshallableParamTypes = multipleNonMarshallableParamTypes; + } + + @Parameterized.Parameters(name = "{0}") + public static Collection wireTypes() { + return Arrays.asList( + new Object[]{null}, + new Object[]{true}, + new Object[]{false} + ); + } + /** + * An interface defining a single method that accepts a String message. + */ + interface PrintObjectSingle { + void msg(Object message); + } + + interface PrintObjectDouble { + void msg(Object key, Container message); + } + + interface PrintPrimitiveDouble { + void msg(long key, Container message); + } + + interface PrintFinalObjectDouble { + void msg(FinalMarshallableContainer m, FinalNonMarshallableContainer nm); + } + + public static final class FinalMarshallableContainer extends NonMarshallableTestContainer implements Marshallable{} + public static final class FinalNonMarshallableContainer extends NonMarshallableTestContainer{} + + public static class MarshallableTestContainer extends NonMarshallableTestContainer implements Marshallable { + } + + public static class NonMarshallableTestContainer implements Container { + + String randomInt = String.valueOf(new Random().nextInt()); + + @Override + public String toString() { + return getClass().getSimpleName() + "{" + randomInt + "}"; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof NonMarshallableTestContainer && randomInt.equals(((NonMarshallableTestContainer)obj).randomInt); + } + + @Override + public int hashCode() { + return randomInt.hashCode(); + } + } + + public interface Container{} + + @Test + public void testSingle() throws Exception { + // Initialization of the wire + Wire w = new BinaryWire(Bytes.allocateElasticOnHeap()); + PrintObjectSingle printer = w.methodWriter(PrintObjectSingle.class); + + // Set up a MethodReader to read the String message and process it using the println method + MethodReader reader = w.methodReaderBuilder() + .multipleNonMarshallableParamTypes(multipleNonMarshallableParamTypes) + .build((PrintObjectSingle) message -> { + singleQ.add(message); + }); + // test with a series of objects + testSingle(printer, reader, new MarshallableTestContainer()); + testSingle(printer, reader, "hello"); + testSingle(printer, reader, new NonMarshallableTestContainer()); + testSingle(printer, reader, new MarshallableTestContainer()); + testSingle(printer, reader, new NonMarshallableTestContainer()); + } + + @Test + public void testDouble() throws Exception { + // Initialization of the wire + Wire w = new TextWire(Bytes.allocateElasticOnHeap()); + PrintObjectDouble printer = w.methodWriter(PrintObjectDouble.class); + + // Set up a MethodReader to read the String message and process it using the println method + MethodReader reader = w.methodReaderBuilder() + .multipleNonMarshallableParamTypes(multipleNonMarshallableParamTypes) + .build((PrintObjectDouble) (key, message) -> { + doubleQ.add(key); + doubleQ.add(message); + }); + // test with a series of objects + testDouble(printer::msg, reader, "key1", new MarshallableTestContainer()); + testDouble(printer::msg, reader, "key2", new NonMarshallableTestContainer()); + testDouble(printer::msg, reader, new MarshallableTestContainer(), new MarshallableTestContainer()); + testDouble(printer::msg, reader, new NonMarshallableTestContainer(), new NonMarshallableTestContainer()); + testDouble(printer::msg, reader, Integer.valueOf(5), new MarshallableTestContainer()); + testDouble(printer::msg, reader, Integer.valueOf(6), new MarshallableTestContainer()); + testDouble(printer::msg, reader, Long.valueOf(3L), new MarshallableTestContainer()); + } + + @Test + public void testDoubleFinal() throws Exception { + // Initialization of the wire + Wire w = new TextWire(Bytes.allocateElasticOnHeap()); + PrintFinalObjectDouble printer = w.methodWriter(PrintFinalObjectDouble.class); + + // Set up a MethodReader to read the String message and process it using the println method + MethodReader reader = w.methodReaderBuilder() + .multipleNonMarshallableParamTypes(multipleNonMarshallableParamTypes) + .build((PrintFinalObjectDouble) (key, message) -> { + doubleQ.add(key); + doubleQ.add(message); + }); + // test with a series of objects + testDouble(printer::msg, reader, new FinalMarshallableContainer(), new FinalNonMarshallableContainer()); + testDouble(printer::msg, reader, new FinalMarshallableContainer(), new FinalNonMarshallableContainer()); + } + + @Test + public void testPrimitive() throws Exception { + // Initialization of the wire + Wire w = new BinaryWire(Bytes.allocateElasticOnHeap()); + PrintPrimitiveDouble printer = w.methodWriter(PrintPrimitiveDouble.class); + + // Set up a MethodReader to read the String message and process it using the println method + MethodReader reader = w.methodReaderBuilder() + .multipleNonMarshallableParamTypes(multipleNonMarshallableParamTypes) + .build((PrintPrimitiveDouble) (key, message) -> { + doubleQ.add(key); + doubleQ.add(message); + }); + // test with a series of objects + testDoubleWithPrimitive(printer, reader, 1L, new MarshallableTestContainer()); + testDoubleWithPrimitive(printer, reader, 2L, new NonMarshallableTestContainer()); + testDoubleWithPrimitive(printer, reader, Long.MAX_VALUE, new MarshallableTestContainer()); + testDoubleWithPrimitive(printer, reader, 2L, new NonMarshallableTestContainer()); + } + + private void testSingle(PrintObjectSingle printer, MethodReader reader, Object obj) throws Exception { + test(() -> printer.msg(obj), reader, singleQ, obj); + } + + private void testDouble(BiConsumer printer, MethodReader reader, K key, C obj) throws Exception { + test(() -> printer.accept(key, obj), reader, doubleQ, key, obj); + } + + private void testDoubleWithPrimitive(PrintPrimitiveDouble printer, MethodReader reader, long key, Container obj) throws Exception { + test(() -> printer.msg(key, obj), reader, doubleQ, key, obj); + } + + private void test(Runnable methodCall, MethodReader reader, ArrayBlockingQueue queue, Object ... objs) throws Exception { + methodCall.run(); + boolean marshallableToNonMarshallable = false; + if (prevObjClasses[0] != null) { + for (int i=0; i < objs.length; i++) { + marshallableToNonMarshallable |= Marshallable.class.isAssignableFrom(prevObjClasses[i]) && !(objs[i] instanceof Marshallable) && !(objs[i] instanceof Number); + } + } + if (Boolean.FALSE.equals(multipleNonMarshallableParamTypes) && marshallableToNonMarshallable) { + Assert.assertThrows(RuntimeException.class, () -> assertWrite(reader, queue, objs)); + } else { + assertWrite(reader, queue, objs); + } + + Assert.assertTrue("Reception Queue should be empty", queue.isEmpty()); + } + + private void assertWrite(MethodReader reader, ArrayBlockingQueue queue, Object ... objs) throws Exception { + reader.readOne(); + + String classMismatchString = ""; + for (int i = 0; i < objs.length; i++) { + Object obj = objs[i]; + // Fetch the read message from the blocking queue with a timeout + Object result = queue.poll(10, TimeUnit.SECONDS); + // Verify that the fetched message matches the expected content + Class objClass = obj.getClass(); + if (objClass != result.getClass()) { + classMismatchString = "Invalid class type! " + objClass.getSimpleName() + " != " + result.getClass().getSimpleName(); + continue; + } + Assert.assertEquals(obj, result); + prevObjClasses[i] = objClass; + Object usedObj = usedObjects.get(i).get(objClass); + if (usedObj != null) { + if (objClass != String.class && !Number.class.isAssignableFrom(objClass) && (!Boolean.FALSE.equals(multipleNonMarshallableParamTypes) || + Marshallable.class.isAssignableFrom(objClass) || Modifier.isFinal(objClass.getModifiers()))) { + Assert.assertSame(usedObj, result); + } else { + Assert.assertNotSame(usedObj, result); + } + } + usedObjects.get(i).put(objClass, result); + } + if (!classMismatchString.isEmpty()) { + throw new RuntimeException(classMismatchString); + } + } +}