All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.openhft.chronicle.wire.GenerateMethodReader Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2016-2020 chronicle.software
 *
 *       https://chronicle.software
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.openhft.chronicle.wire;

import net.openhft.chronicle.bytes.*;
import net.openhft.chronicle.core.Jvm;
import net.openhft.chronicle.core.Maths;
import net.openhft.chronicle.core.io.Closeable;
import net.openhft.chronicle.core.util.GenericReflection;
import net.openhft.chronicle.core.util.IgnoresEverything;
import net.openhft.chronicle.wire.utils.JavaSourceCodeFormatter;
import net.openhft.chronicle.wire.utils.SourceCodeFormatter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;

import static java.lang.String.format;
import static net.openhft.chronicle.core.util.GenericReflection.*;
import static net.openhft.chronicle.wire.GenerateMethodWriter.isSynthetic;

/**
 * Responsible for code generation and its runtime compilation of custom {@link MethodReader}s.
 * The class dynamically generates Java source code based on the provided configurations and compiles them at runtime.
 * It offers the flexibility to create custom MethodReaders tailored to specific needs without manual coding.
 */
public class GenerateMethodReader {

    // 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<>();

    static {
        // Initialization: ensuring Wires classes are loaded and classpath is set up
        Wires.init();
        // Populate IGNORED_INTERFACES with predefined interfaces
        Collections.addAll(IGNORED_INTERFACES,
                BytesMarshallable.class,
                DocumentContext.class,
                ReadDocumentContext.class,
                WriteDocumentContext.class,
                ExcerptListener.class,
                FieldInfo.class,
                FieldNumberParselet.class,
                SelfDescribingMarshallable.class,
                BytesMarshallable.class,
                Marshallable.class,
                MarshallableIn.class,
                MarshallableOut.class,
                MethodWriter.class,
                SourceContext.class
        );
    }

    // Configuration for the type of wire to use for serialization/deserialization.
    private final WireType wireType;

    // 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.
    private final Object[] instances;

    // Interceptor for handling method returns during the method reader generation.
    private final MethodReaderInterceptorReturns interceptor;
    // A mapping to ensure unique method names for the generated code.
    private final Map handledMethodNames = new HashMap<>();
    // A set to store method signatures that have been processed.
    private final Set handledMethodSignatures = new HashSet<>();
    // A set to store interfaces that have been processed.
    private final Set> handledInterfaces = new HashSet<>();
    // Buffers for holding generated Java source code.
    private final SourceCodeFormatter sourceCode = new JavaSourceCodeFormatter();
    private final SourceCodeFormatter fields = new JavaSourceCodeFormatter();
    private final SourceCodeFormatter eventNameSwitchBlock = new JavaSourceCodeFormatter();
    private final SourceCodeFormatter eventNameSwitchBlockMeta = new JavaSourceCodeFormatter();
    private final SourceCodeFormatter eventIdSwitchBlock = new JavaSourceCodeFormatter();
    private final SourceCodeFormatter eventIdSwitchBlockMeta = new JavaSourceCodeFormatter();
    private final SourceCodeFormatter numericConverters = new JavaSourceCodeFormatter();
    // Name of the class that will be generated.
    private final String generatedClassName;
    // Set of field names to ensure uniqueness in the generated code.
    private final Set fieldNames = new LinkedHashSet<>();

    // Flag indicating the presence of a method filter in the generated code.
    private boolean methodFilterPresent;

    // Flag indicating whether the source code has been generated.
    private boolean isSourceCodeGenerated;

    // Flag indicating if there are chained method calls in the generated code.
    private boolean hasChainedCalls;

    /**
     * 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 metaDataHandler Array of meta-data handlers
     * @param instances       Instances that dictate the structure of the generated MethodReader
     */
    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();
    }

    /**
     * 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 type The type under consideration
     * @return A string representing the method's signature
     */
    private static String signature(Method m, Class type) {
        return GenericReflection.getReturnType(m, type) + " " + m.getName() + " " + Arrays.toString(GenericReflection.getParameterTypes(m, type));
    }

    /**
     * Checks if the given class has an "INSTANCE" field.
     * Useful to verify if a class adheres to certain patterns or conventions.
     *
     * @param aClass The class to be checked
     * @return true if the class has an "INSTANCE" field, false otherwise
     */
    static boolean hasInstance(Class aClass) {
        try {
            aClass.getField("INSTANCE");
            return true;
        } catch (NoSuchFieldException e) {
            return false;
        }
    }

    /**
     * Generates and compiles the source code of a custom {@link MethodReader} at runtime.
     * It uses the configurations and instances provided during initialization.
     * If there are issues during compilation, it provides detailed error messages for easier debugging.
     *
     * @return A new class representing the custom MethodReader
     */
    public Class createClass() {
        // If source code isn't already generated, generate it.
        if (!isSourceCodeGenerated)
            generateSourceCode();

        final ClassLoader classLoader = instances[0].getClass().getClassLoader();
        final String fullClassName = packageName() + "." + generatedClassName();

        try {
            return Wires.loadFromJava(classLoader, fullClassName, sourceCode.toString());
        } catch (AssertionError e) {
            if (e.getCause() instanceof LinkageError) {
                try {
                    return Class.forName(fullClassName, true, classLoader);
                } catch (ClassNotFoundException x) {
                    throw Jvm.rethrow(x);
                }
            }
            throw Jvm.rethrow(e);
        } catch (Throwable e) {
            throw Jvm.rethrow(new ClassNotFoundException(e.getMessage() + '\n' + sourceCode, e));
        }
    }

    /**
     * Responsible for generating the source code of a {@link MethodReader} based on specified {@link #instances}.
     * This method encapsulates the logic for building the source code dynamically, by inspecting provided interfaces,
     * filtering ignored interfaces, and appending necessary components to the code.
     */
    private void generateSourceCode() {
        // Clear previously handled interfaces and method signatures for clean generation.
        handledInterfaces.clear();
        handledMethodNames.clear();
        handledMethodSignatures.clear();

        // Handle meta data handlers and their associated interfaces.
        for (int i = 0; metaDataHandler != null && i < metaDataHandler.length; i++) {
            final Class aClass = metaDataHandler[i].getClass();

            // Process each interface of the meta data handler.
            for (Class anInterface : ReflectionUtil.interfaces(aClass)) {
                if (Jvm.dontChain(anInterface))
                    continue;
                handleInterface(anInterface, "metaInstance" + i, false, eventNameSwitchBlockMeta, eventIdSwitchBlockMeta);
            }
        }

        // Clear previously handled interfaces and method signatures again to process instance interfaces.
        handledInterfaces.clear();
        handledMethodNames.clear();
        handledMethodSignatures.clear();

        // Handle instances and their associated interfaces.
        for (int i = 0; i < instances.length; i++) {
            final Class aClass = instances[i].getClass();

            // Check if the instance has method filters.
            boolean methodFilter = instances[i] instanceof MethodFilterOnFirstArg;
            methodFilterPresent |= methodFilter;

            // Process each interface of the instance.
            for (Class anInterface : ReflectionUtil.interfaces(aClass)) {
                if (IGNORED_INTERFACES.contains(anInterface))
                    continue;
                handleInterface(anInterface, "instance" + i, methodFilter, eventNameSwitchBlock, eventIdSwitchBlock);
            }
        }

        // Start constructing the source code.
        if (!packageName().isEmpty())
            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" +
                "import net.openhft.chronicle.core.util.ObjectUtils;\n" +
                "import net.openhft.chronicle.bytes.*;\n" +
                "import net.openhft.chronicle.wire.*;\n" +
                "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");

        // Declare the generated class extending AbstractGeneratedMethodReader.
        sourceCode.append(format("public class %s extends AbstractGeneratedMethodReader {\n", generatedClassName()));

        // Inline comment for instances section.
        sourceCode.append("// instances on which parsed calls are invoked\n");

        for (int i = 0; metaDataHandler != null && i < metaDataHandler.length; i++) {
            sourceCode.append(format("private final Object metaInstance%d;\n", i));
        }
        for (int i = 0; i < instances.length; i++) {
            sourceCode.append(format("private final Object instance%d;\n", i));
        }
        sourceCode.append("private final WireParselet defaultParselet;\n");
        sourceCode.append("\n");

        // Check and declare an interceptor if one is present.
        if (hasRealInterceptorReturns()) {
            sourceCode.append("// method reader interceptor\n");
            sourceCode.append("private final MethodReaderInterceptorReturns interceptor;\n");
            sourceCode.append("\n");
        }

        // Appending fields to the source code.
        sourceCode.append(fields);

        // If a method filter is present, add an ignored flag.
        if (methodFilterPresent) {
            sourceCode.append("// flag for handling ignoreMethodBasedOnFirstArg\n");
            sourceCode.append("private boolean ignored;\n\n");
        }

        // Add numeric converters if any are present.
        if (numericConverters.length() > 0) {
            sourceCode.append("// numeric converters\n");
            sourceCode.append(numericConverters);
            sourceCode.append("\n");
        }

        // Add result for chained calls if any are present.
        if (hasChainedCalls) {
            sourceCode.append("// chained call result\n");
            sourceCode.append("private Object chainedCallReturnResult;");
            sourceCode.append("\n");
        }

        // Define the constructor of the generated class.
        sourceCode.append(format("public %s(MarshallableIn in, " +
                "WireParselet defaultParselet, " +
                "WireParselet debugLoggingParselet, " +
                "MethodReaderInterceptorReturns interceptor, " +
                "Object[] metaInstances, " +
                "Object[] instances) {\n" +
                "super(in, debugLoggingParselet);\n" +
                "this.defaultParselet = defaultParselet;\n", generatedClassName()));

        // Set interceptor if one is present.
        if (hasRealInterceptorReturns())
            sourceCode.append("this.interceptor = interceptor;\n");

        // Initialize metaInstance objects.
        for (int i = 0; metaDataHandler != null && i < metaDataHandler.length; i++)
            sourceCode.append(format("metaInstance%d = metaInstances[%d];\n", i, i));

        // Initialize instance objects.
        for (int i = 0; i < instances.length - 1; i++)
            sourceCode.append(format("instance%d = instances[%d];\n", i, i));

        sourceCode.append(format("instance%d = instances[%d];\n}\n\n", instances.length - 1, instances.length - 1));

        // Override the restIgnored method if chained calls are present.
        if (hasChainedCalls) {
            sourceCode.append("@Override\n" +
                            "public boolean restIgnored() {\n" +
                            "  return chainedCallReturnResult instanceof ")
                    .append(IgnoresEverything.class.getName())
                    .append(";\n" +
                            "}\n");
        }

        // Define the readOneGenerated method for the MethodReader.
        sourceCode.append("@Override\n" +
                "protected MethodReaderStatus readOneGenerated(WireIn wireIn) {\n" +
                "ValueIn valueIn = wireIn.getValueIn();\n" +
                "String lastEventName = \"\";\n" +
                "if (wireIn.bytes().peekUnsignedByte() == BinaryWireCode.FIELD_NUMBER) {\n" +
                "int methodId = (int) wireIn.readEventNumber();\n" +
                "switch (methodId) {\n");

        // Append method ID switch logic.
        addMethodIdSwitch(MethodReader.HISTORY, MethodReader.MESSAGE_HISTORY_METHOD_ID, eventIdSwitchBlock);
        sourceCode.append(eventIdSwitchBlock);

        // Handle default case and read the event name.
        sourceCode.append("default:\n" +
                // Comment explaining the garbage-free nature for low methodIds.
                "lastEventName = Integer.toString(methodId);\n" +
                "break;\n" +
                "}\n" +
                "}\n" +
                "else {\n" +
                "lastEventName = wireIn.readEvent(String.class);\n" +
                "}\n" +
                // Try-catch block for reading method names.
                "try {\n" +
                "if (Jvm.isDebug())\n" +
                "debugLoggingParselet.accept(lastEventName, valueIn);\n" +
                "if (lastEventName == null)\n" +
                "throw new IllegalStateException(\"Failed to read method name or ID\");\n" +
                "switch (lastEventName) {\n");
        // History case logic.
        if (!eventNameSwitchBlock.contains("case \"history\":"))
            sourceCode.append("case MethodReader.HISTORY:\n" +
                    "valueIn.marshallable(messageHistory);\n" +
                    "return MethodReaderStatus.HISTORY;\n\n");

        // Append to the source code based on event name.
        sourceCode.append(eventNameSwitchBlock);

        // Handle default case if the event name isn't recognized.
        sourceCode.append("default:\n" +
                "defaultParselet.accept(lastEventName, valueIn);\n" +
                "return MethodReaderStatus.UNKNOWN;\n" +
                "}\n");

        // Check if we've handled the event successfully.
        if (eventNameSwitchBlock.contains("break;"))
            sourceCode.append("return MethodReaderStatus.KNOWN;\n");
        sourceCode.append("} \n" +
                "catch (InvocationTargetRuntimeException e) {\n" +
                "throw e;\n" +
                "}\n" +
                "}\n");

        // Handle meta-generated events using a similar logic.
        sourceCode.append("@Override\n" +
                "protected MethodReaderStatus readOneMetaGenerated(WireIn wireIn) {\n" +
                "ValueIn valueIn = wireIn.getValueIn();\n" +
                "String lastEventName = \"\";\n" +
                "if (wireIn.bytes().peekUnsignedByte() == BinaryWireCode.FIELD_NUMBER) {\n" +
                "int methodId = (int) wireIn.readEventNumber();\n" +
                "switch (methodId) {\n");

        sourceCode.append(eventIdSwitchBlockMeta);

        sourceCode.append("default:\n" +
                "valueIn.skipValue();\n" +
                "return MethodReaderStatus.UNKNOWN;\n" +
                "}\n" +
                "}\n" +
                "else {\n" +
                "lastEventName = wireIn.readEvent(String.class);\n" +
                "}\n" +
                "try {\n" +
                "if (Jvm.isDebug())\n" +
                "debugLoggingParselet.accept(lastEventName, valueIn);\n" +
                "if (lastEventName == null)\n" +
                "throw new IllegalStateException(\"Failed to read method name or ID\");\n" +
                "switch (lastEventName) {\n" +
                "case MethodReader.HISTORY:\n" +
                "valueIn.marshallable(messageHistory);\n" +
                "return MethodReaderStatus.HISTORY;\n\n");

        // Append to the source code based on meta event name.
        sourceCode.append(eventNameSwitchBlockMeta);

        // Handle default case if the meta event name isn't recognized.
        sourceCode.append("default:\n" +
                "defaultParselet.accept(lastEventName, valueIn);\n" +
                "return MethodReaderStatus.UNKNOWN;\n" +
                "}\n");
        // Check if we've handled the meta event successfully.
        if (eventNameSwitchBlockMeta.contains("break;"))
            sourceCode.append("return MethodReaderStatus.KNOWN;\n");
        sourceCode.append("} \n" +
                "catch (InvocationTargetRuntimeException e) {\n" +
                "throw e;\n" +
                "}\n" +
                "}\n}\n");

        // Set flag indicating source code has been generated.
        isSourceCodeGenerated = true;

        // Dump the generated source code to console if the flag is set.
        if (DUMP_CODE)
            System.out.println(sourceCode);
    }

    /**
     * This method is used to generate code for handling all method calls of a given interface.
     * It processes the methods recursively in case of chained methods.
     *
     * 

It first checks if the given interface should be chained using {@code Jvm.dontChain()} method. * If not, it immediately returns without executing further. It also checks whether the interface * has already been processed. If yes, it immediately returns. * *

Then it proceeds to process all non-static, non-synthetic methods declared in the given * interface but not in {@code java.lang.Object}. * If a method has already been processed, it's skipped. * *

It also validates that the method isn't one of those defined in {@code java.lang.Object}, * if it is, it's skipped. * *

If a method name has already been processed before, it throws an {@code IllegalStateException}. * This is because MethodReader does not support overloaded methods. * *

Finally, it calls {@code handleMethod()} on the current method if it passed all the above checks. * * @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. * @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)) return; if (!handledInterfaces.add(anInterface)) return; for (@NotNull Method m : anInterface.getMethods()) { Class declaringClass = m.getDeclaringClass(); if (declaringClass == Object.class) continue; final int modifiers = m.getModifiers(); if (Modifier.isStatic(modifiers) || isSynthetic(modifiers)) continue; final String signature = signature(m, anInterface); if (!handledMethodSignatures.add(signature)) continue; final String methodName = m.getName(); try { // skip Object defined methods. Object.class.getMethod(methodName, m.getParameterTypes()); continue; } catch (NoSuchMethodException e) { // not an Object method. } if (handledMethodNames.containsKey(methodName)) { throw new IllegalStateException("MethodReader does not support overloaded methods. " + "Method: " + handledMethodNames.get(methodName) + ", and: " + signature); } handledMethodNames.put(methodName, signature); handleMethod(m, anInterface, instanceFieldName, methodFilter, eventNameSwitchBlock, eventIdSwitchBlock); } } /** * This method generates code for handling the call of a specific method. It sets up necessary fields and structures, * prepares parameters, and constructs a switch block for method calls. * *

Initially, it ensures that the method is accessible and obtains its parameter types and return type. * It processes the method parameters and creates fields for storing them. It also checks if the return type of the method * is chainable and updates the state accordingly. * *

If a real interceptor is returned by the method, it creates an array field to store the interceptor's arguments * and also adds a static field to hold a reference to the method itself. * *

Furthermore, if the method is annotated with {@code MethodId}, it extracts the method ID from the annotation * and adds a switch case for this ID to the {@code eventIdSwitchBlock}. * *

Then, it builds a case for the method in the {@code eventNameSwitchBlock}. The structure of this case varies * depending on the number of parameters the method has and if it's marked with {@code MethodFilterOnFirstArg}. * *

If the method's return type is {@code DocumentContext}, it also adds code to copy the method's result to the wire * and close it. * *

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 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) { Jvm.setAccessible(m); // Retrieving parameter and return type information of the method Type[] parameterTypes = getParameterTypes(m, anInterface); Class chainReturnType = erase(getReturnType(m, anInterface)); // Initial setup for chainable return types if (chainReturnType != DocumentContext.class && (!chainReturnType.isInterface() || Jvm.dontChain(chainReturnType))) chainReturnType = null; // Field setup based on method parameters and interceptor if (parameterTypes.length > 0 || hasRealInterceptorReturns()) fields.append(format("// %s\n", m.getName())); // Iterating through method parameters to setup fields for (int i = 0; i < parameterTypes.length; i++) { Class parameterType = erase(parameterTypes[i]); final String typeName = parameterType.getCanonicalName(); String fieldName = m.getName() + "arg" + i; if (fieldNames.add(fieldName)) { if (parameterType == Bytes.class) { fields.append(format("private Bytes %s = Bytes.allocateElasticOnHeap();\n", fieldName)); } 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)); } } } // Handling chainable return types and interceptor returns if (chainReturnType != null) hasChainedCalls = true; if (hasRealInterceptorReturns()) { fields.append(format("private final Object[] interceptor%sArgs = new Object[%d];\n", m.getName(), parameterTypes.length)); String parameterTypesArg = parameterTypes.length == 0 ? "" : ", " + Arrays.stream(parameterTypes) .map(t -> erase(t).getCanonicalName()) .map(s -> s + ".class") .collect(Collectors.joining(", ")); fields.append(format("private static final Method %smethod = lookupMethod(%s.class, \"%s\"%s);\n", m.getName(), anInterface.getCanonicalName(), m.getName(), parameterTypesArg)); } if (parameterTypes.length > 0 || hasRealInterceptorReturns()) fields.append("\n"); // Checking and handling @MethodId annotation final MethodId methodIdAnnotation = Jvm.findAnnotation(m, MethodId.class); if (methodIdAnnotation != null) { int methodId = Maths.toInt32(methodIdAnnotation.value()); addMethodIdSwitch(m.getName(), methodId, eventIdSwitchBlock); } String chainedCallPrefix = chainReturnType != null ? "chainedCallReturnResult = " : ""; // Handling code generation for event name switch block eventNameSwitchBlock.append(format("case \"%s\":\n", m.getName())); if (parameterTypes.length == 0) { eventNameSwitchBlock.append("valueIn.skipValue();\n"); eventNameSwitchBlock.append(methodCall(m, instanceFieldName, chainedCallPrefix, chainReturnType)); } else if (parameterTypes.length == 1) { eventNameSwitchBlock.append(argumentRead(m, 0, false, parameterTypes)); eventNameSwitchBlock.append(methodCall(m, instanceFieldName, chainedCallPrefix, chainReturnType)); } else { if (methodFilter) { eventNameSwitchBlock.append("ignored = false;\n"); eventNameSwitchBlock.append("valueIn.sequence(this, (f, v) -> {\n"); eventNameSwitchBlock.append(argumentRead(m, 0, true, parameterTypes)); eventNameSwitchBlock.append(format("if (((MethodFilterOnFirstArg) f.%s)." + "ignoreMethodBasedOnFirstArg(\"%s\", f.%sarg%d)) {\n", instanceFieldName, m.getName(), m.getName(), 0)); eventNameSwitchBlock.append("f.ignored = true;\n"); for (int i = 1; i < parameterTypes.length; i++) eventNameSwitchBlock.append("v.skipValue();\n"); eventNameSwitchBlock.append("}\n"); eventNameSwitchBlock.append("else {\n"); for (int i = 1; i < parameterTypes.length; i++) eventNameSwitchBlock.append(argumentRead(m, i, true, parameterTypes)); eventNameSwitchBlock.append("}\n"); eventNameSwitchBlock.append("});\n"); eventNameSwitchBlock.append("if (!ignored) {\n"); } else { eventNameSwitchBlock.append("valueIn.sequence(this, (f, v) -> { " + "// todo optimize megamorphic lambda call\n"); for (int i = 0; i < parameterTypes.length; i++) eventNameSwitchBlock.append(argumentRead(m, i, true, parameterTypes)); eventNameSwitchBlock.append("});\n"); } eventNameSwitchBlock.append(methodCall(m, instanceFieldName, chainedCallPrefix, chainReturnType)); if (methodFilter) eventNameSwitchBlock.append("}\n"); } // Handling chainable return types specifically for DocumentContext if (chainReturnType == DocumentContext.class) { eventNameSwitchBlock.append("wireIn.copyTo(((") .append(DocumentContext.class.getName()) .append(") chainedCallReturnResult).wire());\n") .append(Closeable.class.getName()) .append(".closeQuietly(chainedCallReturnResult);\n" + "chainedCallReturnResult = null;\n"); chainReturnType = null; } eventNameSwitchBlock.append("break;\n\n"); // Further handling of chainable return types if (chainReturnType != null) handleInterface(chainReturnType, "chainedCallReturnResult", false, eventNameSwitchBlock, eventIdSwitchBlock); } /** * Creates a switch block in the provided SourceCodeFormatter for a given method ID. * This method facilitates the dynamic selection of methods based on their assigned IDs, allowing * efficient routing and method invocation. * * @param methodName The name of the method for which the switch case is generated. * @param methodId The ID assigned to the method. * @param eventIdSwitchBlock Code block where the generated switch case is appended. */ private void addMethodIdSwitch(String methodName, int methodId, SourceCodeFormatter eventIdSwitchBlock) { // Append the switch case based on method ID eventIdSwitchBlock.append(format("case %d:\n", methodId)); // Set the last executed method's name eventIdSwitchBlock.append(format("lastEventName = \"%s\";\n", methodName)); // End of the switch case eventIdSwitchBlock.append("break;\n\n"); } /** * Generates code that invokes passed method, saves method return value (in case it's a chained call) * and handles {@link MethodReaderInterceptorReturns} if it's specified. * * @param m Method that is being processed. * @param instanceFieldName In generated code, method is executed on field with this name. * @param chainedCallPrefix Prefix for method call statement, passed in order to save method result for chaining. * @return Code that performs a method call. */ private String methodCall(Method m, String instanceFieldName, String chainedCallPrefix, @Nullable Class returnType) { StringBuilder res = new StringBuilder(); Class[] parameterTypes = m.getParameterTypes(); // Array to store argument references used for method call String[] args = new String[parameterTypes.length]; for (int i = 0; i < parameterTypes.length; i++) args[i] = m.getName() + "arg" + i; // Begin try block for method invocation res.append("try {\n"); // Flag indicating a data event has been processed res.append("dataEventProcessed = true;\n"); // called for no interceptor and a generating interceptor if (!hasRealInterceptorReturns()) { // Potential cast to the generating interceptor type GeneratingMethodReaderInterceptorReturns generatingInterceptor = interceptor != null ? (GeneratingMethodReaderInterceptorReturns) interceptor : null; // If a generating interceptor is present, append the code before method call if (generatingInterceptor != null) { final String codeBefore = generatingInterceptor.codeBeforeCall(m, instanceFieldName, args); if (codeBefore != null) res.append(codeBefore).append("\n"); } // Generate the method call code res.append(format("%s((%s) %s).%s(%s);%n", chainedCallPrefix, m.getDeclaringClass().getCanonicalName(), instanceFieldName, m.getName(), String.join(", ", args))); // If a generating interceptor is present, append the code after method call if (generatingInterceptor != null) { final String codeAfter = generatingInterceptor.codeAfterCall(m, instanceFieldName, args); if (codeAfter != null) res.append(codeAfter).append("\n"); } } else { // called for non generating interceptor for (int i = 0; i < parameterTypes.length; i++) { res.append(format("interceptor%sArgs[%d] = %sarg%d;\n", m.getName(), i, m.getName(), i)); } // Determine if a return type cast is needed for the method String castPrefix = chainedCallPrefix.isEmpty() || returnType == null ? "" : "(" + returnType.getCanonicalName() + ")"; // Generate the code for interceptor invocation res.append(format("%s%sinterceptor.intercept(%smethod, %s, " + "interceptor%sArgs, this::actualInvoke);\n", chainedCallPrefix, castPrefix, m.getName(), instanceFieldName, m.getName())); } // Catch block for handling exceptions during method invocation res.append("} \n" + "catch (Exception e) {\n" + "throw new InvocationTargetRuntimeException(e);\n" + "}\n"); return res.toString(); } /** * Generates code for reading an argument of a method from a {@link ValueIn} object. * The argument's index and type, and whether it is read in a lambda function, * 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 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 */ private String argumentRead(Method m, int argIndex, boolean inLambda, Type[] parameterTypes) { Class numericConversionClass = null; // Numeric conversion is not supported for binary wire if (wireType == WireType.TEXT || wireType == WireType.YAML) { Annotation[] annotations = m.getParameterAnnotations()[argIndex]; // Loop through annotations to identify and set the numeric conversion class. for (Annotation a : annotations) { if (a instanceof LongConversion) { numericConversionClass = ((LongConversion) a).value(); break; } else { // For other annotations, find if they have an associated LongConversion. LongConversion lc = Jvm.findAnnotation(a.annotationType(), LongConversion.class); if (lc != null) { numericConversionClass = lc.value(); break; } } } } // Determine the type of the argument being read. final Class argumentType = erase(parameterTypes[argIndex]); String trueArgumentName = m.getName() + "arg" + argIndex; // Decide on the naming pattern based on whether the read is happening inside a lambda. String argumentName = (inLambda ? "f." : "") + trueArgumentName; String valueInName = inLambda ? "v" : "valueIn"; // Generate code based on the type of the argument. if (boolean.class.equals(argumentType)) { return format("%s = %s.bool();\n", argumentName, valueInName); } else if (byte.class.equals(argumentType)) { // If numeric conversion is available and has a shared instance. if (numericConversionClass != null && hasInstance(numericConversionClass)) { return format("%s = (byte) %s.INSTANCE.parse(%s.text());\n", argumentName, numericConversionClass.getName(), valueInName); } // If numeric conversion is available and is an instance of LongConverter. else if (numericConversionClass != null && LongConverter.class.isAssignableFrom(numericConversionClass)) { // Register a converter for this type. numericConverters.append(format("private final %s %sConverter = ObjectUtils.newInstance(%s.class);\n", numericConversionClass.getCanonicalName(), trueArgumentName, numericConversionClass.getCanonicalName())); return format("%s = (byte) %sConverter.parse(%s.text());\n", argumentName, argumentName, valueInName); } else // Default byte reading logic. return format("%s = %s.readByte();\n", argumentName, valueInName); } else if (char.class.equals(argumentType)) { // Handling character type arguments. return format("%s = %s.character();\n", argumentName, valueInName); } else if (short.class.equals(argumentType)) { // Generate code based on the type of the argument and presence of numeric conversion. if (numericConversionClass != null && hasInstance(numericConversionClass)) { return format("%s = (short) %s.INSTANCE.parse(%s.text());\n", argumentName, numericConversionClass.getName(), valueInName); } else if (numericConversionClass != null && LongConverter.class.isAssignableFrom(numericConversionClass)) { numericConverters.append(format("private final %s %sConverter = ObjectUtils.newInstance(%s.class);\n", numericConversionClass.getCanonicalName(), trueArgumentName, numericConversionClass.getCanonicalName())); return format("%s = (short) %sConverter.parse(%s.text());\n", argumentName, argumentName, valueInName); } else return format("%s = %s.int16();\n", argumentName, valueInName); } else if (int.class.equals(argumentType)) { // Generate code based on the type of the argument and presence of numeric conversion. if (numericConversionClass != null && hasInstance(numericConversionClass)) { return format("%s = (int) %s.INSTANCE.parse(%s.text());\n", argumentName, numericConversionClass.getName(), valueInName); } else if (numericConversionClass != null && LongConverter.class.isAssignableFrom(numericConversionClass)) { numericConverters.append(format("private final %s %sConverter = ObjectUtils.newInstance(%s.class);\n", numericConversionClass.getCanonicalName(), trueArgumentName, numericConversionClass.getCanonicalName())); return format("%s = (int) %sConverter.parse(%s.text());\n", argumentName, argumentName, valueInName); } else return format("%s = %s.int32();\n", argumentName, valueInName); } else if (long.class.equals(argumentType)) { // Generate code based on the type of the argument and presence of numeric conversion. if (numericConversionClass != null && hasInstance(numericConversionClass)) { return format("%s = %s.INSTANCE.parse(%s.text());\n", argumentName, numericConversionClass.getName(), valueInName); } else if (numericConversionClass != null && LongConverter.class.isAssignableFrom(numericConversionClass)) { numericConverters.append(format("private final %s %sConverter = ObjectUtils.newInstance(%s.class);\n", numericConversionClass.getCanonicalName(), trueArgumentName, numericConversionClass.getCanonicalName())); return format("%s = %sConverter.parse(%s.text());\n", argumentName, argumentName, valueInName); } else { return format("%s = %s.int64();\n", argumentName, valueInName); } } else if (float.class.equals(argumentType)) { // Handling float type arguments. return format("%s = %s.float32();\n", argumentName, valueInName); } else if (double.class.equals(argumentType)) { // Handling double type arguments. return format("%s = %s.float64();\n", argumentName, valueInName); } else if (Bytes.class.isAssignableFrom(argumentType)) { // Handling Bytes type arguments. return format("%s.bytes(%s);\n", valueInName, argumentName); } else if (CharSequence.class.isAssignableFrom(argumentType)) { // Handling CharSequence type arguments. return format("%s = %s.text();\n", argumentName, valueInName); } else { // Handling other object types. final String typeName = argumentType.getCanonicalName(); 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(%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. * * @return {@code true} if there's a real interceptor present, {@code false} otherwise. */ private boolean hasRealInterceptorReturns() { return interceptor != null && !(interceptor instanceof GeneratingMethodReaderInterceptorReturns); } /** * Retrieves the package name for the generated class. * The package name is determined based on the class name of the first instance. * * @return The package name of the generated class. */ public String packageName() { Class firstClass = instances[0].getClass(); String firstClassFullName = firstClass.getName(); return ReflectionUtil.generatedPackageName(firstClassFullName); } /** * Gets the simple name of the generated class. * * @return The simple name of the generated class. */ public String generatedClassName() { return generatedClassName; } /** * Constructs the generated class name using various components such as * 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. */ @NotNull private String generatedClassName0() { final StringBuilder sb = new StringBuilder(); // Append names of instances to the class name. for (Object i : instances) appendInstanceName(sb, i); // Append names of metadata handlers if available. if (metaDataHandler != null) for (Object i : metaDataHandler) appendInstanceName(sb, i); // Append wire type to the class name. if (wireType != null) 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()); else if (hasRealInterceptorReturns()) sb.append("Intercepting"); sb.append("MethodReader"); // Convert slashes for nested or inner classes. return sb.toString().replace("/", "$"); } /** * Appends the simplified name of the instance's class to the provided StringBuilder. * The method takes into account various scenarios including: * - If the class is a proxy class. * - If the class is enclosed within another class. * - If the class is synthetic, anonymous, or local. * The method aims to provide a more concise and meaningful name for the instance's class * that can be used in contexts like generating a class name. * * @param sb The StringBuilder to which the instance name should be appended. * @param i The instance for which the class name is determined. */ private void appendInstanceName(StringBuilder sb, Object i) { Class aClass = i.getClass(); // Check if it's a proxy class. if (Proxy.isProxyClass(aClass)) { aClass = aClass.getInterfaces()[0]; } // Check for enclosing class. if (aClass.getEnclosingClass() != null) sb.append(aClass.getEnclosingClass().getSimpleName()); String name = Jvm.isLambdaClass(aClass) ? aClass.getInterfaces()[0].getName() : aClass.getName(); final int packageDelimiterIndex = name.lastIndexOf('.'); // Intentionally using this instead of class.simpleName() in order to support anonymous class String nameWithoutPackage = packageDelimiterIndex == -1 ? name : name.substring(packageDelimiterIndex + 1); // Further refine the name for specific synthetic class scenarios. if (aClass.isSynthetic() && !aClass.isAnonymousClass() && !aClass.isLocalClass()) { int lambdaSlashIndex = nameWithoutPackage.lastIndexOf("/"); if (lambdaSlashIndex != -1) nameWithoutPackage = nameWithoutPackage.substring(0, lambdaSlashIndex); } // Append the refined name to the StringBuilder. sb.append(nameWithoutPackage); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy