/* GNU LESSER GENERAL PUBLIC LICENSE Copyright (C) 2006 The Lobo Project This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Contact info: lobochief@users.sourceforge.net */ package org.lobobrowser.js; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; import org.lobobrowser.util.Objects; import org.mozilla.javascript.Callable; import org.mozilla.javascript.Context; import org.mozilla.javascript.EvaluatorException; import org.mozilla.javascript.Function; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.WrappedException; import org.w3c.dom.DOMException; public class JavaFunctionObject extends ScriptableObject implements Function { private static final long serialVersionUID = 3716471130167741876L; private static final Logger logger = Logger.getLogger(JavaFunctionObject.class.getName()); private static final boolean loggableInfo = logger.isLoggable(Level.INFO); private final String methodName; private final String className; private final ArrayList<Method> methods = new ArrayList<>(); public JavaFunctionObject(final String name, final String className) { super(); this.methodName = name; this.className = className; // TODO: Review // Quick hack for issue #98 defineProperty("call", new Callable() { public Object call(final Context cx, final Scriptable scope, final Scriptable thisObj, final Object[] args) { if ((args.length > 0) && (args[0] instanceof JavaObjectWrapper)) { final JavaObjectWrapper javaObjectWrapper = (JavaObjectWrapper) args[0]; return JavaFunctionObject.this.call(cx, scope, javaObjectWrapper, Arrays.copyOfRange(args, 1, args.length)); } else { throw new RuntimeException("Unexpected condition"); } } }, org.mozilla.javascript.ScriptableObject.READONLY); } public void addMethod(final Method m) { this.methods.add(m); } @Override public String getClassName() { return "JavaFunctionObject"; } /* private static String getTypeName(final Object object) { return object == null ? "[null]" : object.getClass().getName(); }*/ private final static class MethodAndArguments { private final Method method; private final Object[] args; public MethodAndArguments(final Method method, final Object[] args) { this.method = method; this.args = args; } public Object invoke(final Object javaObject) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { return method.invoke(javaObject, args); } @Override public String toString() { return "MethodAndArguments [method=" + method + ", args=" + Arrays.toString(args) + "]"; } } private MethodAndArguments getExactMethod(final Object[] args) { final ArrayList<Method> methods = this.methods; final int size = methods.size(); for (int i = 0; i < size; i++) { final Method m = methods.get(i); final Class<?>[] parameterTypes = m.getParameterTypes(); if (args == null) { if ((parameterTypes == null) || (parameterTypes.length == 0)) { return new MethodAndArguments(m, null); } } else if (parameterTypes != null) { if (args.length == parameterTypes.length) { if (Objects.areSameTo(args, parameterTypes)) { return new MethodAndArguments(m, args); } } else if ((parameterTypes.length == 1) && parameterTypes[0].isArray()) { final Class<?> arrayType = parameterTypes[0].getComponentType(); final boolean allSame = true; for (int j = 0; j < args.length; j++) { if (!Objects.isSameOrBox(args[j], arrayType)) { break; } } if (allSame) { final Object[] argsInArray = (Object[]) Array.newInstance(arrayType, args.length); for (int j = 0; j < args.length; j++) { argsInArray[j] = args[j]; } return new MethodAndArguments(m, new Object[] { argsInArray }); } } } } return null; } private MethodAndArguments getBestMethod(final Object[] args) { final MethodAndArguments exactMethod = getExactMethod(args); if (exactMethod != null) { return exactMethod; } final ArrayList<Method> methods = this.methods; final int size = methods.size(); int matchingNumParams = 0; Method matchingMethod = null; for (int i = 0; i < size; i++) { final Method m = methods.get(i); final Class<?>[] parameterTypes = m.getParameterTypes(); if (args == null) { if ((parameterTypes == null) || (parameterTypes.length == 0)) { return new MethodAndArguments(m, new Object[0]); } } else if ((parameterTypes != null) && (args.length >= parameterTypes.length)) { if (Objects.areAssignableTo(args, parameterTypes)) { final Object[] actualArgs = convertArgs(args, parameterTypes.length, parameterTypes); return new MethodAndArguments(m, actualArgs); } if ((matchingMethod == null) || (parameterTypes.length > matchingNumParams)) { matchingNumParams = parameterTypes.length; matchingMethod = m; } } } if (size == 0) { throw new IllegalStateException("zero methods"); } if (matchingMethod == null) { return null; } else { final Class<?>[] actualArgTypes = matchingMethod.getParameterTypes(); final Object[] actualArgs = convertArgs(args, matchingNumParams, actualArgTypes); return new MethodAndArguments(matchingMethod, actualArgs); } } private static Object[] convertArgs(final Object[] args, final int numConvert, final Class<?>[] actualArgTypes) { final JavaScript manager = JavaScript.getInstance(); final Object[] actualArgs = args == null ? new Object[0] : new Object[numConvert]; if (args != null) { for (int i = 0; i < numConvert; i++) { final Object arg = args[i]; actualArgs[i] = manager.getJavaObject(arg, actualArgTypes[i]); } } return actualArgs; } public Object call(final Context cx, final Scriptable scope, final Scriptable thisObj, final Object[] args) { final MethodAndArguments methodAndArguments = this.getBestMethod(args); if (methodAndArguments == null) { throw new EvaluatorException("No method matching " + this.methodName + " with " + (args == null ? 0 : args.length) + " arguments in " + className + " ."); } final JavaScript manager = JavaScript.getInstance(); try { if (thisObj instanceof JavaObjectWrapper) { final JavaObjectWrapper jcw = (JavaObjectWrapper) thisObj; // if(linfo) { // Object javaObject = jcw.getJavaObject(); // logger.info("call(): Calling method " + method.getName() + // " on object " + javaObject + " of type " + // this.getTypeName(javaObject)); // } final Object raw = methodAndArguments.invoke(jcw.getJavaObject()); return manager.getJavascriptObject(raw, scope); } else { // if (args[0] instanceof Function ) { // Function func = (Function) args[0]; // Object raw = func.call(cx, scope, scope, Arrays.copyOfRange(args, 1, // args.length)); // return manager.getJavascriptObject(raw, scope); // } else { final Object raw = methodAndArguments.invoke(thisObj); return manager.getJavascriptObject(raw, scope); // } // Based on http://stackoverflow.com/a/16479685/161257 // return call(cx, scope, getParentScope(), args); } } catch (final IllegalAccessException iae) { throw new IllegalStateException("Unable to call " + this.methodName + ".", iae); } catch (final InvocationTargetException ite) { if (ite.getCause() instanceof DOMException) { final DOMException domException = (DOMException) ite.getCause(); throw new WrappedException(domException); } throw new WrappedException( new InvocationTargetException(ite.getCause(), "Unable to call " + this.methodName + " on " + thisObj + ".")); } catch (final IllegalArgumentException iae) { final StringBuffer argTypes = new StringBuffer(); for (int i = 0; i < methodAndArguments.args.length; i++) { if (i > 0) { argTypes.append(", "); } argTypes.append(methodAndArguments.args[i] == null ? "<null>" : methodAndArguments.args[i].getClass().getName()); } throw new WrappedException(new IllegalArgumentException("Unable to call " + this.methodName + " in " + className + ". Argument types: " + argTypes + "." + "\n on method: " + methodAndArguments.method, iae)); } } @Override public java.lang.Object getDefaultValue(final java.lang.Class<?> hint) { if (loggableInfo) { logger.info("getDefaultValue(): hint=" + hint + ",this=" + this); } if ((hint == null) || String.class.equals(hint)) { return "function " + this.methodName; } else { return super.getDefaultValue(hint); } } public Scriptable construct(final Context cx, final Scriptable scope, final Object[] args) { throw new UnsupportedOperationException(); } }