org.grouplens.lenskit.eval.script.ConfigMethodInvoker.java Source code

Java tutorial

Introduction

Here is the source code for org.grouplens.lenskit.eval.script.ConfigMethodInvoker.java

Source

/*
 * 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 <a href="http://www.grouplens.org">GroupLens Research</a>
 */
@SuppressWarnings("rawtypes")
public class ConfigMethodInvoker {
    private final EvalScriptEngine engine;
    private final EvalProject project;
    private final Map<Object, List<ListenableFuture<?>>> 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<ListenableFuture<?>> deps = objectDependencies.get(obj);
        if (deps == null) {
            deps = Lists.newLinkedList();
            objectDependencies.put(obj, deps);
        }
        deps.add(dep);
    }

    public synchronized List<ListenableFuture<?>> getDeps(Object obj) {
        List<ListenableFuture<?>> 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<Method> getOneArgMethods(Object obj, final String name) {
        return Iterables.filter(Arrays.asList(obj.getClass().getMethods()), new Predicate<Method>() {
            @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<ListenableFuture<?>> deps = getDeps(builder);
        clearDeps(builder);
        if (deps.isEmpty()) {
            return builder.build();
        } else {
            ListenableFuture<List<Object>> ideps = Futures.allAsList(deps);
            if (ideps.isDone()) {
                return builder.build();
            } else {
                return Futures.transform(ideps, new Function<List<Object>, Object>() {
                    @Nullable
                    @Override
                    public Object apply(@Nullable List<Object> input) {
                        return builder.build();
                    }
                });
            }
        }
    }

    Object transform(Object obj, Function<Object, ?> 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<Object> findBuildableMethod(final Object self, String name, final Object[] args) {
        Supplier<Object> result = null;
        for (final Method method : getOneArgMethods(self, name)) {
            Class<?> param = method.getParameterTypes()[0];
            final Class<? extends Builder> 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<Object>() {
                    @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<Object> 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<Object> 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<Object>() {
                        @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<Object> 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<Object>() {
                        @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<Object>() {
                @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
     *         {@var args} does not have end with a closure, {@code Pair.of(args, null)} is returned.
     */
    public Pair<Object[], Closure> 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.
     *
     * <p>If the object has a {@code setEvalConfig} method, that method is called with the project's
     * configuration.  Likewise, an {@code setEvalProject} property or {@code set{roject} 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> T constructAndConfigure(Class<T> type, Object[] args) throws NoSuchMethodException {
        Pair<Object[], Closure> 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 <strong>not</strong> 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<Object, Object> recur = new Function<Object, Object>() {
                    @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<Object> 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;
    }
}