private Object invokeMethod()

in gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/JavaTranslator.java [227:353]


    private Object invokeMethod(final Object delegate, final Class<?> returnType, final String methodName, final Object... arguments) {
        // populate method cache for fast access to methods in subsequent calls
        final Map<String, List<ReflectedMethod>> methodCache = GLOBAL_METHOD_CACHE.getOrDefault(delegate.getClass(), new HashMap<>());
        if (methodCache.isEmpty()) buildMethodCache(delegate, methodCache);

        // create a copy of the argument array so as not to mutate the original bytecode - no need to create a new
        // object if there are no arguments.
        final Object[] argumentsCopy = arguments.length > 0 ? new Object[arguments.length] : arguments;
        for (int i = 0; i < arguments.length; i++) {
            argumentsCopy[i] = translateObject(arguments[i]);
        }

        // without this initial check iterating an invalid methodName will lead to a null pointer and a less than
        // great error message for the user. 
        if (!methodCache.containsKey(methodName)) {
            throw new IllegalStateException(generateMethodNotFoundMessage(
                    "Could not locate method", delegate, methodName, argumentsCopy));
        }

        boolean found = false;
        try {
            for (final ReflectedMethod methodx : methodCache.get(methodName)) {
                final Method method = methodx.method;
                if (returnType.isAssignableFrom(method.getReturnType())) {
                    final Parameter[] parameters = methodx.parameters;
                    if (parameters.length == argumentsCopy.length || methodx.hasVarArgs) {
                        final Object[] newArguments = new Object[parameters.length];
                        found = true;
                        for (int i = 0; i < parameters.length; i++) {
                            if (parameters[i].isVarArgs()) {
                                final Class<?> parameterClass = parameters[i].getType().getComponentType();
                                if (argumentsCopy.length > i && argumentsCopy[i] != null && !parameterClass.isAssignableFrom(argumentsCopy[i].getClass())) {
                                    found = false;
                                    break;
                                }
                                final Object[] varArgs = (Object[]) Array.newInstance(parameterClass, argumentsCopy.length - i);
                                int counter = 0;
                                for (int j = i; j < argumentsCopy.length; j++) {
                                    varArgs[counter++] = argumentsCopy[j];
                                }
                                newArguments[i] = varArgs;
                                break;
                            } else {
                                // try to detect the right method by comparing the type of the parameter to the type
                                // of the argument. doesn't always work so well because of null arguments which don't
                                // bring their type in bytecode and rely on position. this doesn't seem to happen often
                                // ...luckily...because method signatures tend to be sufficiently unique and do not
                                // encourage whacky use - like g.V().has(null, null) is clearly invalid so we don't
                                // even need to try to sort that out. on the other hand g.V().has('name',null) which
                                // is valid hits like four different possible overloads, but we can rely on the most
                                // generic one which takes Object as the second parameter. that seems to work in this
                                // case, but it's a shame this isn't nicer. seems like nicer would mean a heavy
                                // overhaul to Gremlin or to GLVs/bytecode and/or to serialization mechanisms.
                                //
                                // the check where argumentsCopy[i] is null could be accompanied by a type check for
                                // allowable signatures like:
                                // null == argumentsCopy[i] && parameters[i].getType() == Object.class
                                // but that doesn't seem helpful. perhaps this approach is fine as long as we ensure
                                // consistency of null calls to all overloads. in other words addV(String) must behave
                                // the same as addV(Traversal) if null is used as the argument. so far, that seems to
                                // be the case. if we find that is not happening we either fix that specific
                                // inconsistency, start special casing those method finds here, or as mentioned above
                                // do something far more drastic that doesn't involve reflection.
                                if (i < argumentsCopy.length && (null == argumentsCopy[i] ||
                                        (argumentsCopy[i] != null && (
                                                parameters[i].getType().isAssignableFrom(argumentsCopy[i].getClass()) ||
                                                        (parameters[i].getType().isPrimitive() &&
                                                                (Number.class.isAssignableFrom(argumentsCopy[i].getClass()) ||
                                                                        argumentsCopy[i].getClass().equals(Boolean.class) ||
                                                                        argumentsCopy[i].getClass().equals(Byte.class) ||
                                                                        argumentsCopy[i].getClass().equals(Character.class))))))) {
                                    newArguments[i] = argumentsCopy[i];
                                } else {
                                    found = false;
                                    break;
                                }
                            }
                        }

                        // special cases of has() where the first arg is null or third arg is null - sometimes this can end up with the T being
                        // null or in the second case calling has that accepts predicate instead of string as parameter in the last argument.
                        // That generates an exception which raises badly in the translator. it is
                        // safer to force this to the String form by letting this "found" version pass. In Java
                        // form of GraphTraversal generated in the first case  can't be produced because of validations
                        // for has(T, ...) but in other language it might be allowed which means that has((T) null, ...) from something like
                        // python will end up as has((String) null, ...) which filters rather than generates an
                        // exception. calling the T version even in a non-JVM language seems odd and more likely the
                        // caller was shooting for the String one, but ugh who knows. this issue showcases again how
                        // badly bytecode should either change to use gremlin-language and go away or bytecode should
                        // get a safer way to be translated to a traversal with more explicit calls that don't rely
                        // on reflection.
                        Class<?>[] parametersTypes = method.getParameterTypes();
                        if (methodName.equals(GraphTraversal.Symbols.has) && newArguments.length > 0) {
                            //first case has((T)null, ...) instead of has((String)null, ...) 
                            if (null == newArguments[0] &&
                                    parametersTypes[0].isAssignableFrom(org.apache.tinkerpop.gremlin.structure.T.class)) {
                                found = false;
                            } else if (newArguments.length == 3 && newArguments[2] == null && parametersTypes[0] == String.class &&
                                    parametersTypes[1] == String.class &&
                                    parametersTypes[2] == P.class) {
                                //the second case has(String, String, (P)(null)) instead of has(String,String, (Object)null)
                                found = false;
                            }
                        }
                        if (found) {
                            return 0 == newArguments.length ? method.invoke(delegate) : method.invoke(delegate, newArguments);
                        }
                    }
                }
            }
        } catch (final Throwable e) {
            if (found && (e instanceof InvocationTargetException)) {
                // get the target exception out for a better message, otherwise, it will be null.
                final InvocationTargetException ite = (InvocationTargetException) e;
                throw new IllegalStateException(generateMethodNotFoundMessage(
                        ite.getTargetException().getMessage(), null, methodName, argumentsCopy), ite.getTargetException());
            } else {
                throw new IllegalStateException(generateMethodNotFoundMessage(
                        e.getMessage(), null, methodName, argumentsCopy), e);
            }
        }

        // if it got down here then the method was in the cache but it was never called as it could not be found
        // for the supplied arguments
        throw new IllegalArgumentException(generateMethodNotFoundMessage(
                "Could not locate exact method given the supplied arguments", delegate, methodName, argumentsCopy));
    }