sf.net.experimaestro.manager.scripting.GenericFunction.java Source code

Java tutorial

Introduction

Here is the source code for sf.net.experimaestro.manager.scripting.GenericFunction.java

Source

package sf.net.experimaestro.manager.scripting;

/*
 * This file is part of experimaestro.
 * Copyright (c) 2014 B. Piwowarski <benjamin@bpiwowar.net>
 *
 * experimaestro is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * experimaestro 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with experimaestro.  If not, see <http://www.gnu.org/licenses/>.
 */

import org.apache.commons.lang.ClassUtils;
import org.apache.commons.lang.mutable.MutableInt;
import org.mozilla.javascript.NativeObject;
import org.mozilla.javascript.ScriptRuntime;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.Wrapper;
import sf.net.experimaestro.exceptions.WrappedException;
import sf.net.experimaestro.exceptions.XPMRhinoException;
import sf.net.experimaestro.manager.js.JSBaseObject;
import sf.net.experimaestro.manager.js.JavaScriptContext;
import sf.net.experimaestro.manager.json.Json;
import sf.net.experimaestro.utils.JSUtils;
import sf.net.experimaestro.utils.Output;
import sf.net.experimaestro.utils.arrays.ListAdaptator;

import java.lang.reflect.Array;
import java.lang.reflect.Executable;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.function.Function;

import static java.lang.Math.max;
import static java.lang.StrictMath.min;

/**
 * Base class for scripting methods or constructors
 */
public abstract class GenericFunction {

    static private final Function IDENTITY = Function.identity();
    static private final Function<Wrapper, Object> UNWRAPPER = x -> x.unwrap();
    static private final Function<Object, String> TOSTRING = x -> x.toString();

    /**
     * Transform the arguments
     *
     * @param cx          The script context
     * @param lcx
     * @param declaration
     * @param args
     * @param offset      The offset within the target parameters    @return
     */
    static Object[] transform(LanguageContext lcx, ScriptContext cx, Declaration declaration, Object[] args,
            Function[] converters, int offset) {
        final Executable executable = declaration.executable();
        final Class<?>[] types = executable.getParameterTypes();
        Object methodArgs[] = new Object[types.length];

        // --- Add context and scope if needed
        Expose annotation = executable.getAnnotation(Expose.class);
        if (annotation != null && (annotation.scope() && annotation.context())) {
            throw new UnsupportedOperationException(
                    "Annotations scope and context cannot be used at the same time");
        }

        if (annotation == null ? false : annotation.scope()) {
            JavaScriptContext jcx = (JavaScriptContext) lcx;
            methodArgs[0] = jcx.context();
            methodArgs[1] = jcx.scope();
        }

        if (annotation == null ? false : annotation.context()) {
            methodArgs[0] = lcx;
            methodArgs[1] = cx;
        }

        // --- Copy the non vararg parameters
        final int length = types.length - (executable.isVarArgs() ? 1 : 0) - offset;
        int size = min(length, args.length);
        for (int i = 0; i < size; i++) {
            methodArgs[i + offset] = converters[i].apply(args[i]);
        }

        // --- Deals with the vararg pararameters
        if (executable.isVarArgs()) {
            final Class<?> varargType = types[types.length - 1].getComponentType();
            int nbVarargs = args.length - length;
            final Object array[] = (Object[]) Array.newInstance(varargType, nbVarargs);
            for (int i = 0; i < nbVarargs; i++) {
                array[i] = converters[i + length].apply(args[i + length]);
            }
            methodArgs[methodArgs.length - 1] = array;
        }

        return methodArgs;
    }

    /**
     * Gives a score to a given declaration
     *
     * @param cx          The script context
     * @param declaration The underlying method or constructor
     * @param args        The arguments
     * @param converters  A list of converters that will be filled by this method
     * @param offset      The offset for the converters
     * @return A score (minimum integer if no conversion is possible)
     */
    static int score(LanguageContext lcx, ScriptContext cx, Declaration declaration, Object[] args,
            Function[] converters, MutableInt offset) {

        final Executable executable = declaration.executable();
        final Class<?>[] types = executable.getParameterTypes();
        final boolean isVarArgs = executable.isVarArgs();

        // Get the annotations
        Expose annotation = declaration.executable.getAnnotation(Expose.class);
        final boolean contextAnnotation = annotation == null ? false : annotation.context();
        final boolean scopeAnnotation = annotation == null ? false : annotation.scope();
        int optional = annotation == null ? 0 : annotation.optional();

        // Start the scoring
        Converter converter = new Converter();

        // Offset in the types
        offset.setValue(contextAnnotation || scopeAnnotation ? 2 : 0);

        // Number of "true" arguments (not scope, not vararg)
        final int nbArgs = types.length - offset.intValue() - (isVarArgs ? 1 : 0);

        // The number of arguments should be in:
        // [nbArgs - optional, ...] if varargs
        // [nbArgs - optional, nbArgs] otherwise

        if (args.length < nbArgs - optional)
            return Integer.MIN_VALUE;

        if (!isVarArgs && args.length > nbArgs)
            return Integer.MIN_VALUE;

        // If the optional arguments are at the beginning, then shift
        if (annotation != null && annotation.optionalsAtStart()) {
            offset.add(max(nbArgs - args.length, 0));
        }

        // Normal arguments
        for (int i = 0; i < args.length && i < nbArgs && converter.isOK(); i++) {
            final Object o = args[i];
            converters[i] = converter.converter(lcx, cx, o, types[i + offset.intValue()]);
        }

        // Var args
        if (isVarArgs) {
            Class<?> type = ClassUtils.primitiveToWrapper(types[types.length - 1].getComponentType());
            int nbVarArgs = args.length - nbArgs;
            for (int i = 0; i < nbVarArgs && converter.isOK(); i++) {
                final Object o = args[nbArgs + i];
                converters[nbArgs + i] = converter.converter(lcx, cx, o, type);
            }
        }

        return converter.score;
    }

    /**
     * Get the name of the method or constructor
     */
    protected abstract String getName();

    protected abstract <T extends Declaration> Iterable<T> declarations();

    public Object call(LanguageContext lcx, ScriptContext cx, Object thisObj, Object[] args) {
        Declaration argmax = null;
        int max = Integer.MIN_VALUE;

        Function argmaxConverters[] = new Function[args.length];
        Function converters[] = new Function[args.length];
        int argMaxOffset = 0;

        for (Declaration method : declarations()) {
            MutableInt offset = new MutableInt(0);
            int score = score(lcx, cx, method, args, converters, offset);
            if (score > max) {
                max = score;
                argmax = method;
                Function tmp[] = argmaxConverters;
                argMaxOffset = offset.intValue();
                argmaxConverters = converters;
                converters = tmp;
            }
        }

        if (argmax == null) {
            String context = "";
            if (thisObj instanceof JSBaseObject)
                context = " in an object of class " + ClassDescription.getClassName(thisObj.getClass());

            throw ScriptRuntime.typeError(String.format("Could not find a matching method for %s(%s)%s", getName(),
                    Output.toString(", ", args, o -> o.getClass().toString()), context));
        }

        // Call the constructor
        try {
            Object[] transformedArgs = transform(lcx, cx, argmax, args, argmaxConverters, argMaxOffset);
            final Object result = argmax.invoke(transformedArgs);

            return result;
        } catch (InvocationTargetException e) {
            if (e.getCause() instanceof XPMRhinoException) {
                throw (XPMRhinoException) e.getCause();
            }
            throw new WrappedException(new XPMRhinoException(e.getCause()));
        } catch (Throwable e) {
            throw new WrappedException(new XPMRhinoException(e));
        }

    }

    abstract static public class Declaration<T extends Executable> {
        private final T executable;

        public Declaration(T executable) {
            this.executable = executable;
        }

        public Executable executable() {
            return executable;
        }

        public abstract Object invoke(Object[] transformedArgs)
                throws InvocationTargetException, IllegalAccessException, InstantiationException;

    }

    static public class ListConverter implements Function {
        private final Class<?> arrayClass;
        ArrayList<Function> functions = new ArrayList<>();

        public ListConverter(Class<?> arrayClass) {
            this.arrayClass = arrayClass;
        }

        @Override
        public Object apply(Object input) {
            final Collection collection = (Collection) input;
            final Object[] objects = (Object[]) Array.newInstance(arrayClass, functions.size());
            final Iterator iterator = collection.iterator();
            int i = 0;
            while (iterator.hasNext()) {
                objects[i] = functions.get(i).apply(iterator.next());
                ++i;
            }
            assert i == objects.length;
            return objects;
        }

        public void add(Function function) {
            functions.add(function);
        }
    }

    /**
     * A converter
     */
    static public class Converter {
        int score = Integer.MAX_VALUE;

        Function converter(LanguageContext lcx, ScriptContext cx, Object o, Class<?> type) {
            if (o == null) {
                score--;
                return IDENTITY;
            }

            // Assignable: OK
            type = ClassUtils.primitiveToWrapper(type);
            if (type.isAssignableFrom(o.getClass())) {
                if (o.getClass() != type)
                    score--;
                if (o instanceof Wrapper)
                    return object -> ((Wrapper) object).unwrap();
                return IDENTITY;
            }

            // Arrays
            if (type.isArray()) {
                Class<?> innerType = type.getComponentType();

                if (o.getClass().isArray())
                    o = ListAdaptator.create((Object[]) o);

                if (o instanceof Collection) {
                    final Collection array = (Collection) o;
                    final Iterator iterator = array.iterator();
                    final ListConverter listConverter = new ListConverter(innerType);

                    while (iterator.hasNext()) {
                        listConverter.add(converter(lcx, cx, iterator.next(), innerType));
                        if (score == Integer.MIN_VALUE) {
                            return null;
                        }
                    }

                    return listConverter;

                }
            }

            // Case of string: anything can be converted, but with different
            // scores
            if (type == String.class) {
                if (o instanceof Scriptable) {
                    switch (((Scriptable) o).getClassName()) {
                    case "String":
                    case "ConsString":
                        return TOSTRING;
                    default:
                        score -= 10;
                    }
                } else if (o instanceof CharSequence) {
                    score--;
                } else {
                    score -= 10;
                }
                return TOSTRING;
            }

            // Cast to integer
            if (type == Integer.class && o instanceof Number) {
                if ((((Number) o).intValue()) == ((Number) o).doubleValue()) {
                    return input -> ((Number) input).intValue();
                }
            }

            // Native object to JSON
            if (o instanceof NativeObject && Json.class.isAssignableFrom(type)) {
                score -= 10;
                JavaScriptContext jcx = (JavaScriptContext) lcx;
                return nativeObject -> JSUtils.toJSON(jcx.scope(), nativeObject);
            }

            // Everything else failed... unwrap and try again
            if (o instanceof Wrapper) {
                score -= 1;
                Function converter = converter(lcx, cx, ((Wrapper) o).unwrap(), type);
                return converter != null ? UNWRAPPER.andThen(converter) : null;
            }

            score = Integer.MIN_VALUE;
            return null;
        }

        public boolean isOK() {
            return score != Integer.MIN_VALUE;
        }

    }
}