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));
}