org.grouplens.lenskit.eval.script.EvalScriptEngine Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of lenskit-eval Show documentation
Show all versions of lenskit-eval Show documentation
Facilities for evaluating recommender algorithms.
/*
* 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.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import groovy.lang.Binding;
import groovy.lang.GroovyRuntimeException;
import groovy.lang.GroovyShell;
import groovy.lang.MissingPropertyException;
import org.apache.commons.lang3.builder.Builder;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.customizers.ImportCustomizer;
import org.grouplens.lenskit.util.ClassDirectory;
import org.grouplens.lenskit.eval.EvalProject;
import org.grouplens.lenskit.eval.EvalTask;
import org.grouplens.lenskit.eval.TaskExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.URL;
import java.util.*;
/**
* Load and process configuration files. Also provides helper methods used by the
* configuration scripts to locate & invoke methods.
*
* @author GroupLens Research
* @since 0.10
*/
public class EvalScriptEngine {
private static Logger logger = LoggerFactory.getLogger(EvalScriptEngine.class);
private static final String METHOD_PATH = "META-INF/lenskit-eval/methods/";
protected ClassLoader classLoader;
protected ClassDirectory directory;
protected GroovyShell shell;
@Nullable
protected final Properties properties;
@SuppressWarnings("rawtypes")
private final Map builders = new HashMap();
/**
* Construct a new script engine. The engine uses the current thread's classloader.
*/
public EvalScriptEngine() {
this(Thread.currentThread().getContextClassLoader());
}
/**
* Construct a new script engine.
* @param loader The class loader to use.
*/
public EvalScriptEngine(ClassLoader loader) {
this(loader, null);
}
/**
* Construct a new script engine.
* @param loader The class loader to use.
* @param props Additional properties to use when creating new projects.
* @see org.grouplens.lenskit.eval.EvalProject#EvalProject(java.util.Properties)
*/
public EvalScriptEngine(ClassLoader loader, @Nullable Properties props) {
CompilerConfiguration compConfig = new CompilerConfiguration(CompilerConfiguration.DEFAULT);
properties = props;
compConfig.setScriptBaseClass("org.grouplens.lenskit.eval.script.EvalScript");
ImportCustomizer imports = new ImportCustomizer();
imports.addStarImports("org.grouplens.lenskit",
"org.grouplens.lenskit.params",
"org.grouplens.lenskit.baseline",
"org.grouplens.lenskit.norm",
"org.grouplens.lenskit.eval.metrics.predict",
"org.grouplens.lenskit.eval.metrics.recommend");
compConfig.addCompilationCustomizers(imports);
shell = new GroovyShell(loader, new Binding(), compConfig);
classLoader = loader;
loadExternalMethods();
directory = ClassDirectory.forClassLoader(loader);
}
/**
* Create a new eval project.
* @return The eval project.
*/
public EvalProject createProject() {
return new EvalProject(properties, classLoader);
}
//region Loading and running scripts
/**
* Run a script from a file.
*
* @param file The file to run.
* @param project The project to run the script against.
* @return The script as parsed and compiled by Groovy.
* @throws IOException if the file cannot be read.
*/
public Object runScript(File file, EvalProject project) throws IOException, TaskExecutionException {
EvalScript script = (EvalScript) shell.parse(file);
return runScript(script, project);
}
/**
* Run a script from a reader.
*
* @param in The reader to read.
* @param project The project to run the script against.
* @return The script as parsed and compiled by Groovy.
*/
public Object runScript(Reader in, EvalProject project) throws IOException {
EvalScript script;
try {
script = (EvalScript) shell.parse(in);
} catch (GroovyRuntimeException e) {
if (e.getCause() != null) {
Throwables.propagateIfInstanceOf(e.getCause(), IOException.class);
}
throw e;
}
return script;
}
/**
* Run an evaluation config script and get the evaluations it produces.
*
* @param script The script to run (as loaded by Groovy)
* @param project The project to run the script on.
* @return The return value of the script.
* @throws org.grouplens.lenskit.eval.TaskExecutionException if the script is invalid or produces an error.
*/
@Nullable
public Object runScript(EvalScript script, EvalProject project) throws TaskExecutionException {
script.setEngine(this);
script.setProject(project);
Object result = null;
try {
result = script.run();
} catch (MissingPropertyException e) {
String name = e.getProperty();
Set packages = directory.getPackages(name);
logger.error("Cannot resolve class or property " + name);
if (!packages.isEmpty()) {
logger.info("Did you intend to import it from {}?", Joiner.on(", ").join(packages));
}
throw new TaskExecutionException("unresolvable property " + name, e);
} catch (RuntimeException e) {
Throwables.propagateIfInstanceOf(e.getCause(), TaskExecutionException.class);
throw new TaskExecutionException("error running configuration script", e);
}
return result;
}
/**
* Load a set of evaluations from a script file.
*
* @param file A Groovy script to configure the evaluator.
* @return A list of evaluations to run.
* @throws org.grouplens.lenskit.eval.TaskExecutionException if there is a configuration error
* @throws IOException if there is an error reading the file
*/
public EvalProject loadProject(File file) throws TaskExecutionException, IOException {
logger.debug("loading script file {}", file);
EvalProject project = new EvalProject(properties, classLoader);
runScript(file, project);
return project;
}
/**
* Load a set of evaluations from an input stream.
*
* @param in The input stream
* @return A list of evaluations
* @throws org.grouplens.lenskit.eval.TaskExecutionException if there is a configuration error
*/
public Object loadProject(Reader in) throws TaskExecutionException, IOException {
EvalProject project = createProject();
runScript(in, project);
return project;
}
//endregion
//region External method lookup
private Class extends R> lookupMethod(Class root, String key, String name) {
// FIXME Cache these lookups
String path = METHOD_PATH + name + ".properties";
logger.debug("loading method {} from {}", name, path);
try {
InputStream istr = classLoader.getResourceAsStream(path);
if (istr == null) {
logger.debug("path {} not found", path);
return null;
}
try {
Properties props = new Properties();
props.load(istr);
Object pv = props.get(key);
String className = pv == null ? null : pv.toString();
if (className == null) {
return null;
}
return classLoader.loadClass(className).asSubclass(root);
} finally {
istr.close();
}
} catch (IOException e) {
throw new RuntimeException("error reading method " + name, e);
} catch (ClassNotFoundException e) {
throw new RuntimeException("cannot find command class", e);
}
}
/**
* Look up a registered method of any type. The currently supported types are {@linkplain Builder builders}
* and {@linkplain org.grouplens.lenskit.eval.EvalTask tasks}.
* @param name The method name.
* @return The method implementation class, or {@code null} if it the method is not found.
*/
public Class> lookupMethod(@Nonnull String name) {
Class> task = lookupTask(name);
Class> builder = lookupBuilder(name);
if (task == null && builder == null) {
return null;
} else if (task != null) {
if (builder == null) {
return task;
} else {
throw new RuntimeException("ambiguous method " + name);
}
} else {
return builder;
}
}
/**
* Find a task with a particular name if it exists.
*
* @param name The name of the command
* @return The command factory or {@code null} if no such factory exists.
*/
@SuppressWarnings("rawtypes")
@CheckForNull
@Nullable
public Class extends EvalTask> lookupTask(@Nonnull String name) {
return lookupMethod(EvalTask.class, "task", name);
}
/**
* Find a builder with a particular name if it exists.
*
* @param name The name of the command
* @return The command factory or {@code null} if no such factory exists.
*/
@SuppressWarnings("rawtypes")
@CheckForNull
@Nullable
public Class extends Builder> lookupBuilder(@Nonnull String name) {
return lookupMethod(Builder.class, "builder", name);
}
/**
* Get a command for a type. It consults registered commands and looks for the
* {@link BuiltBy} annotation.
*
* @param type A type that needs to be built.
* @return A command class to build {@code type}, or {@code null} if none can be found.
* @see #registerCommand
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public Class extends Builder> getBuilderForType(Class type) {
@SuppressWarnings("rawtypes")
Class builder = builders.get(type);
if (builder == null) {
BuiltBy annot = type.getAnnotation(BuiltBy.class);
if (annot != null) {
builder = annot.value();
}
}
return builder;
}
/**
* Register a builder class for a type. Used to allow commands to be found for types where
* the type cannot be augmented with the {@link BuiltBy} annotation.
*
* @param type The type to build.
* @param command A class that can build instances of {@code type}.
* @param The type to build (type parameter).
*/
@SuppressWarnings("rawtypes")
public void registerCommand(Class type, Class extends Builder> command) {
Preconditions.checkNotNull(type, "type cannot be null");
Preconditions.checkNotNull(command, "command cannot be null");
builders.put(type, command);
}
/**
* Register a default set of external methods.
*/
@SuppressWarnings({"rawtypes", "unchecked"})
protected void loadExternalMethods() {
Properties props = new Properties();
try {
for (URL url : Collections.list(classLoader.getResources("META-INF/lenskit-eval/builders.properties"))) {
InputStream istr = url.openStream();
try {
props.load(istr);
} finally {
istr.close();
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
for (Map.Entry
© 2015 - 2024 Weber Informatics LLC | Privacy Policy