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

org.grouplens.lenskit.eval.script.ConfigMethodInvoker Maven / Gradle / Ivy

There is a newer version: 3.0-T5
Show newest version
/*
 * LensKit, an open source recommender systems toolkit.
 * Copyright 2010-2014 LensKit Contributors.  See CONTRIBUTORS.md.
 * Work on LensKit has been funded by the National Science Foundation under
 * grants IIS 05-34939, 08-08692, 08-12148, and 10-17697.
 *
 * This program 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 program 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
 * this program; if not, write to the Free Software Foundation, Inc., 51
 * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package org.grouplens.lenskit.eval.script;

import com.google.common.base.*;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import groovy.lang.Closure;
import groovy.lang.GroovyRuntimeException;
import groovy.lang.MetaClass;
import groovy.lang.MetaMethod;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.Builder;
import org.apache.commons.lang3.reflect.ConstructorUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.grouplens.lenskit.config.GroovyUtils;
import org.grouplens.lenskit.eval.EvalConfig;
import org.grouplens.lenskit.eval.EvalProject;
import org.grouplens.lenskit.util.Functional;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import static com.google.common.util.concurrent.JdkFutureAdapters.listenInPoolThread;

/**
 * Utilities for searching for methods of configurable objects.
 *
 * @author GroupLens Research
 */
@SuppressWarnings("rawtypes")
public class ConfigMethodInvoker {
    private final EvalScriptEngine engine;
    private final EvalProject project;
    private final Map>> objectDependencies = Maps.newIdentityHashMap();

    public ConfigMethodInvoker(@Nonnull EvalScriptEngine engine, @Nonnull EvalProject project) {
        this.engine = engine;
        this.project = project;
    }

    public synchronized void registerDep(Object obj, ListenableFuture dep) {
        Preconditions.checkArgument(obj != dep, "Object cannot depend on itself");
        List> deps = objectDependencies.get(obj);
        if (deps == null) {
            deps = Lists.newLinkedList();
            objectDependencies.put(obj, deps);
        }
        deps.add(dep);
    }

    public synchronized List> getDeps(Object obj) {
        List> deps = objectDependencies.get(obj);
        if (deps == null) {
            return Collections.emptyList();
        } else {
            return ImmutableList.copyOf(deps);
        }
    }

    public synchronized void clearDeps(Object obj) {
        objectDependencies.remove(obj);
    }

    Iterable getOneArgMethods(Object obj, final String name) {
        return Iterables.filter(Arrays.asList(obj.getClass().getMethods()), new Predicate() {
            @Override
            public boolean apply(@Nullable Method method) {
                if (method == null) {
                    return false;
                } else {
                    return method.getName().equals(name) && method.getParameterTypes().length == 1;
                }
            }
        });
    }

    Object finishBuilder(final Builder builder) {
        List> deps = getDeps(builder);
        clearDeps(builder);
        if (deps.isEmpty()) {
            return builder.build();
        } else {
            ListenableFuture> ideps = Futures.allAsList(deps);
            if (ideps.isDone()) {
                return builder.build();
            } else {
                return Futures.transform(ideps, new Function, Object>() {
                    @Nullable
                    @Override
                    public Object apply(@Nullable List input) {
                        return builder.build();
                    }
                });
            }
        }
    }

    @SuppressWarnings("unchecked")
    Object transform(Object obj, Function function) {
        if (obj instanceof Future) {
            return Futures.transform(listenInPoolThread((Future) obj), function);
        } else {
            return function.apply(obj);
        }
    }

    /**
     * Search for a method with a specified BuiltBy, or a single-argument method with a parameter that
     * can be built. Used when we have a closure to build a directive argument.
     *
     * @param self    The command to search.
     * @param args    The arguments.
     * @return A closure to prepare and invoke the method, or {@code null} if no such method can be
     *         found.
     * @see org.grouplens.lenskit.eval.script.EvalScriptEngine#getBuilderForType(Class)
     */
    private Supplier findBuildableMethod(final Object self, String name, final Object[] args) {
        Supplier result = null;
        for (final Method method: getOneArgMethods(self, name)) {
            Class param = method.getParameterTypes()[0];
            final Class bldClass;
            BuiltBy annot = method.getAnnotation(BuiltBy.class);
            if (annot == null) {
                annot = param.getAnnotation(BuiltBy.class);
            }
            if (annot != null) {
                bldClass = annot.value();
            } else {
                bldClass = engine.getBuilderForType(param);
            }

            if (bldClass != null) {
                if (result != null) {
                    throw new RuntimeException("multiple buildable methods named " + name);
                }
                result = new Supplier() {
                    @Override
                    public Object get() {
                        Builder builder;
                        try {
                            builder = constructAndConfigure(bldClass, args);
                            Object val = transform(finishBuilder(builder),
                                                   Functional.invokeMethod(method, self));
                            if (val != self && val instanceof ListenableFuture) {
                                registerDep(self, (ListenableFuture) val);
                            }
                            return val;
                        } catch (NoSuchMethodException e) {
                            throw Throwables.propagate(e);
                        }
                    }
                };
            }
        }

        return result;
    }

    /**
     * Find a method that should be invoked multiple times, if the argument is iterable.  The
     * argument may be iterated multiple times.
     *
     * @param self The configurable object.
     * @param name The method name.
     * @param args The arguments.
     * @return A thunk that will invoke the method.
     */
    private Supplier findMultiMethod(final Object self, String name, final Object[] args) {
        if (args.length != 1) return null;
        // the argument is a list
        final Object arg = args[0];
        if (!(arg instanceof Iterable)) {
            return null;
        }

        final Iterable objects = (Iterable) arg;

        Supplier result = null;
        for (final Method method: getOneArgMethods(self, name)) {
            Class ptype = method.getParameterTypes()[0];
            boolean good = Iterables.all(objects, Predicates.or(Predicates.isNull(),
                                                                Predicates.instanceOf(ptype)));
            if (good) {
                if (result != null) {
                    throw new RuntimeException("multiple compatible methods named " + name);
                } else {
                    result = new Supplier() {
                        @Override
                        public Object get() {
                            for (Object obj: objects) {
                                try {
                                    method.invoke(self, obj);
                                } catch (IllegalAccessException e) {
                                    throw Throwables.propagate(e);
                                } catch (InvocationTargetException e) {
                                    if (e.getCause() != null) {
                                        throw Throwables.propagate(e);
                                    }
                                }
                            }
                            return null;
                        }
                    };
                }
            }
        }

        return result;
    }

    /**
     * Look for a method on an object.
     *
     * @param self The object.
     * @param name The method name.
     * @param args The method arguments.
     * @return A thunk invoking the method, or {@code null} if no such method is found.
     */
    private Supplier findMethod(final Object self, String name, Object[] args) {
        Object[] objects = Arrays.copyOf(args, args.length);
        Class[] types = new Class[args.length];
        for (int i = 0; i < args.length; i++) {
            if (objects[i] != null) {
                types[i] = objects[i].getClass();
            }
        }

        MetaClass metaclass = InvokerHelper.getMetaClass(self);

        MetaMethod mm = metaclass.pickMethod(name, types);

        // try some simple transformations
        // transform a trailing closure to a function
        if (mm == null && objects.length > 0) {
            Object lastArg = objects[objects.length - 1];
            if (lastArg instanceof Closure) {
                Class[] at2 = Arrays.copyOf(types, types.length);
                at2[objects.length - 1] = Function.class;
                mm = metaclass.pickMethod(name, at2);
                if (mm != null) {
                    objects[objects.length - 1] =  new ClosureFunction((Closure) lastArg);
                }
            }
        }


        // try instantiating a single class
        if (mm == null && objects.length == 1 && objects[0] instanceof Class) {
            final Class cls = (Class) objects[0];
            Class[] at2 = {cls};
            final MetaMethod method = metaclass.pickMethod(name, at2);

            if (method != null) {
                try {
                    final Constructor ctor = cls.getConstructor();
                    return new Supplier() {
                        @Override
                        public Object get() {
                            Object[] objs;
                            try {
                                objs = new Object[]{ctor.newInstance()};
                            } catch (InstantiationException e) {
                                throw new RuntimeException("cannot instantiate " + cls, e);
                            } catch (IllegalAccessException e) {
                                throw new RuntimeException("cannot instantiate " + cls, e);
                            } catch (InvocationTargetException e) {
                                throw new RuntimeException("cannot instantiate " + cls, e);
                            }
                            return method.doMethodInvoke(self, objs);
                        }
                    };
                } catch (NoSuchMethodException e) {
                    /* no constructor avaialble, ignore */
                }
            }
        }

        if (mm == null) {
            return null;
        } else {
            final MetaMethod method = mm;
            final Object[] finalArgs = objects;
            return new Supplier() {
                @Override
                public Object get() {
                    return method.doMethodInvoke(self, finalArgs);
                }
            };
        }
    }

    private Object makeConfigDelegate(final Object target) {
        ConfigDelegate annot = target.getClass().getAnnotation(ConfigDelegate.class);
        if (annot == null) {
            return new DefaultConfigDelegate(this, target);
        } else {
            Class dlgClass = annot.value();
            try {
                return ConstructorUtils.invokeConstructor(dlgClass, target);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException("error constructing " + dlgClass, e);
            } catch (InvocationTargetException e) {
                throw new RuntimeException("error constructing " + dlgClass, e);
            } catch (InstantiationException e) {
                throw new RuntimeException("error constructing " + dlgClass, e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException("error constructing " + dlgClass, e);
            }
        }
    }

    /**
     * Split an array of arguments into arguments a trailing closure.
     *
     * @param args The argument array.
     * @return A pair consisting of the arguments, except for any trailing closure, and the closure. If
     *         args does not have end with a closure, {@code Pair.of(args, null)} is returned.
     */
    public Pair splitClosure(Object[] args) {
        if (args.length > 0 && args[args.length - 1] instanceof Closure) {
            return Pair.of(Arrays.copyOf(args, args.length - 1),
                           (Closure) args[args.length - 1]);
        } else {
            return Pair.of(args, null);
        }
    }

    /**
     * Construct and configure a configurable object.  This instantiates the class, using the provided
     * arguments.  If the last argument is a closure, it is witheld and used to configure the object
     * after it is constructed.  No extra type coercion is performed.
     *
     * 

If the object has a {@code setEvalConfig} method, that method is called with the project's * configuration. Likewise, an {@code setEvalProject} property or {@code setProject} method with * type assignable from {@link EvalProject} is set to the project. * * @param type The type to construct. * @param args The arguments. * @return The constructed and configured object. */ private T constructAndConfigure(Class type, Object[] args) throws NoSuchMethodException { Pair split = splitClosure(args); MetaClass metaclass = InvokerHelper.getMetaClass(type); Object obj; try { obj = metaclass.invokeConstructor(split.getLeft()); } catch (GroovyRuntimeException e) { Throwables.propagateIfInstanceOf(e.getCause(), NoSuchMethodException.class); throw e; } metaclass = InvokerHelper.getMetaClass(obj); MetaMethod mm = metaclass.getMetaMethod("setEvalConfig", new Class[]{EvalConfig.class}); if (mm != null) { mm.invoke(obj, new Object[]{project.getConfig()}); } mm = metaclass.getMetaMethod("setEvalProject", new Class[]{EvalProject.class}); if (mm == null) { mm = metaclass.getMetaMethod("setProject", new Class[]{EvalProject.class}); } if (mm != null) { mm.invoke(obj, new Object[]{project}); } if (split.getRight() != null) { GroovyUtils.callWithDelegate(split.getRight(), makeConfigDelegate(obj)); } return type.cast(obj); } /** * Find an external method (a builder or task) and return a closure that, when invoked, * constructs and configures it. It does not invoke the builder or task, that * is left up to the caller. * * @param name The method name. * @return The constructed and configured object corresponding to this method. */ public Object callExternalMethod(String name, Object... args) throws NoSuchMethodException { final Class mtype = engine.lookupMethod(name); if (mtype != null) { try { return constructAndConfigure(mtype, args); } catch (NoSuchMethodException e) { throw new RuntimeException("cannot find suitable for " + mtype.toString(), e); } } else { throw new NoSuchMethodException(name); } } public Object invokeConfigurationMethod(final Object target, final String name, Object... args) { Preconditions.checkNotNull(target, "target object"); if (args.length == 1 && args[0] instanceof Future) { Future f = (Future) args[0]; if (f.isDone()) { try { Object arg = f.get(); return invokeConfigurationMethod(target, name, arg); } catch (InterruptedException e) { throw new RuntimeException("interrupted waiting for dependency", e); } catch (ExecutionException e) { throw new RuntimeException(e.getCause()); } } else { Function recur = new Function() { @Nullable @Override public Object apply(@Nullable Object input) { return invokeConfigurationMethod(target, name, input); } }; ListenableFuture f2 = Futures.transform(listenInPoolThread(f), recur); registerDep(target, f2); return f2; } } final String setterName = "set" + StringUtils.capitalize(name); final String adderName = "add" + StringUtils.capitalize(name); Supplier inv; // directly invoke inv = findMethod(target, name, args); if (inv == null) { inv = findBuildableMethod(target, name, args); } // invoke a setter if (inv == null) { inv = findMethod(target, setterName, args); } // invoke a buildable setter if (inv == null) { inv = findBuildableMethod(target, setterName, args); } // invoke an adder if (inv == null) { inv = findMethod(target, adderName, args); } // add from a list if (inv == null) { inv = findMultiMethod(target, adderName, args); } // invoke a buildable adder if (inv == null) { inv = findBuildableMethod(target, adderName, args); } if (inv != null) { return inv.get(); } else { // try to invoke the method directly return DefaultGroovyMethods.invokeMethod(target, name, args); } } private static class ClosureFunction implements Function { public ClosureFunction(Closure cl) { closure = cl; } @Override public Object apply(Object input) { return closure.call(input); } private Closure closure; @Override public String toString() { return "closure " + closure; } } }