Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.grouplens.lenskit.eval.script.ConfigMethodInvoker Maven / Gradle / Ivy
/*
* 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 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() {
@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;
}
}
}