package net.sourceforge.seqware.pipeline.runner;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import joptsimple.NonOptionArgumentSpec;
import joptsimple.OptionException;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import net.sourceforge.seqware.common.metadata.Metadata;
import net.sourceforge.seqware.common.metadata.MetadataFactory;
import net.sourceforge.seqware.common.module.ReturnValue;
import net.sourceforge.seqware.common.util.Log;
import net.sourceforge.seqware.common.util.configtools.ConfigTools;
import net.sourceforge.seqware.common.util.exceptiontools.ExceptionTools;
import net.sourceforge.seqware.pipeline.plugin.Plugin;
import net.sourceforge.seqware.pipeline.plugin.PluginInterface;
import org.openide.util.Lookup;
/**
* <p>
* PluginRunner class.
* </p>
*
* @author briandoconnor@gmail.com
* @since 20110925
*
* The PluginRunner is a command line utility that will provide a mechanism to extend the core functionality of SeqWare Pipeline in a
* more organized way then the current collection of Perl and other utilities used haphazardly in this subproject. The idea is that
* core SeqWare Pipeline functionality should be implemented in Java and packaged as a single jar file making the code more
* self-contained and easier to package up and install. This class is the browser and caller for plugins that are designed to extend
* the functionality of SeqWare pipeline. These include everything from utilities that uncompress and install workflow bundles, to
* the runner that calls modules, and to utilities designed to query the metadb and perform some utility such as deleting processing
* events and cleaning up files. Unlike the modules ({@link net.sourceforge.seqware.pipeline.module.ModuleInterface}), plugins are
* intended to be written by core SeqWare Pipeline developers and to exist within the source repository of the project not loaded
* from external jar files. Also unlike the module runner ({@link net.sourceforge.seqware.pipeline.runner.Runner}) this plugin runner
* does not save any state to the MetaDB, it's up to the plugin to do that but a metadata object (of the type specified in the users
* SEQWARE_SETTINGS file) is handed off to the plugin for its use.
* @version $Id: $Id
*/
public class PluginRunner {
private Map<String, String> config;
private OptionParser parser = new OptionParser();
private OptionSet options = null;
private Plugin plugin = null;
private Metadata meta = null;
private HashMap<String, ArrayList<String>> map = new HashMap<>();
/**
* <p>
* main.
* </p>
*
* @param args
* an array of {@link java.lang.String} objects.
*/
public static void main(String[] args) {
try {
new PluginRunner().run(args);
} catch (ExitException e) {
System.exit(e.getExitCode());
}
}
private NonOptionArgumentSpec<String> nonOptionSpec;
/**
* <p>
* run.
* </p>
*
* @param args
* an array of {@link java.lang.String} objects.
*/
public void run(String[] args) {
// Specific to the Plugin Runner
// Setup the command line options supported by the PluginRunner
setupOptions();
// Parse the options
try {
options = parser.parse(args);
} catch (OptionException e) {
getSyntax(parser, e.getMessage());
}
// Do syntax check on runner args
checkArguments();
// read the metadata config
setupConfig();
// open a metadata object
setupMetadata();
// list plugins if that option was given (and then exit)
listPlugins();
// These are specific to the plugin if specified
// setup the plugin by passing config
setupPlugin();
// Call each method
invokePluginMethods();
}
/**
* Setup the options parser.
*/
private void setupOptions() {
parser.acceptsAll(Arrays.asList("help", "h", "?"), "Provides this help message.");
parser.acceptsAll(Arrays.asList("list", "l"), "Lists all the plugins available in this SeqWare Pipeline jar file.");
parser.acceptsAll(Arrays.asList("plugin", "p"), "The plugin you wish to trigger.").withRequiredArg();
this.nonOptionSpec = parser
.nonOptions("Specify arguments for the plugin by providding an additional -- and then --<key> <value> pairs");
parser.accepts("verbose", "Show debug information");
}
/**
*
* @param parser
* The options parser object
* @param errorMessage
* Error message
*/
private void getSyntax(OptionParser parser, String errorMessage) {
if (errorMessage != null && errorMessage.length() > 0) {
Log.stdout("ERROR: " + errorMessage);
Log.stdout("");
}
PluginRunner it = new PluginRunner();
String seqwareVersion = it.getClass().getPackage().getImplementationVersion();
Log.stdout("Syntax: java seqware-distribution-" + seqwareVersion
+ "-full.jar [[--help]] [--list] [--verbose] [--plugin] PluginName -- [PluginParameters]");
Log.stdout("");
Log.stdout("--> PluginParameters are passed directly to the Plugin and ignored by the PluginRunner. ");
Log.stdout("--> You must pass '--' right after the PluginName in order to prevent the parameters from being parsed by the PluginRunner!");
Log.stdout("");
Log.stdout("PluginRunner parameters are limited to the following:");
try {
parser.printHelpOn(System.err);
} catch (IOException e) {
e.printStackTrace(System.err);
}
throw new ExitException(ReturnValue.INVALIDARGUMENT);
}
/**
* Do all the syntax here
*/
private void checkArguments() {
// Check if help was requested
if ((options.has("help") || options.has("h") || options.has("?")) && !options.has("plugin") && !options.has("p")) {
getSyntax(parser, "");
throw new ExitException(ReturnValue.RETURNEDHELPMSG);
}
// FIXME: check single char options too
if (options.has("list") && options.has("plugin")) {
Log.error("You can't have both --list and --plugin defined at the same time!");
throw new ExitException(ReturnValue.INVALIDARGUMENT);
}
// FIXME: check single char options too
if (!(options.has("list") || options.has("plugin") || options.has("help"))) {
getSyntax(parser, "You need to specify at least one of --list, --plugin, or --help!");
throw new ExitException(ReturnValue.INVALIDARGUMENT);
}
// check if verbose was requested, then override the log4j.properties
if (options.has("verbose")) {
Log.setVerbose(true);
}
}
/**
* LEFT OFF HERE: take a look at http://download.oracle.com/javase/6/docs/api/java/util/ServiceLoader.html and
* http://wiki.netbeans.org/AboutLookup http://download.netbeans.org/netbeans/7.0.1/final/bundles/netbeans-7.0.1-ml-javase-linux.sh
* http://weblogs.java.net/blog/timboudreau/archive/2008/08/simple_dependen.html This method uses reflection to list all the available
* plugins in this SeqWare Pipeline jar. There's a good tutorial here:
* http://hulles.supersized.org/archives/23-Tips-on-Using-Lookup-in-NetBeans.html There's an alternative project that may avoid the
* compile time issues with Lookup: http://code.google.com/p/spi/
*/
private void listPlugins() {
Collection<? extends PluginInterface> plugs;
plugs = Lookup.getDefault().lookupAll(PluginInterface.class);
// PluginInterface p = (PluginInterface)Lookup.getDefault().lookup(PluginInterface.class);
if (options.has("list") || options.has("l")) {
Log.info("Plugin List:\n");
}
for (PluginInterface plug : plugs) {
if (options.has("list") || options.has("l")) {
Log.stdout(" Plugin: " + plug.getClass().getPackage().getName() + "." + plug.getClass().getSimpleName());
Log.stdout(" " + plug.get_description() + "\n");
}
ArrayList<String> classList = map.get(plug.getClass().getPackage().getName());
if (classList == null) {
classList = new ArrayList<>();
}
classList.add(plug.getClass().getSimpleName());
map.put(plug.getClass().getPackage().getName(), classList);
}
}
/**
* This method loads the plugin class and then passes the command line parameters to it.
*/
private void setupPlugin() {
String pluginName = null;
if (options.has("plugin")) {
pluginName = (String) options.valueOf("plugin");
Log.debug("Running Plugin: " + pluginName);
try {
plugin = (Plugin) Class.forName(pluginName).newInstance();
} catch (ClassNotFoundException e) {
Log.error("Could not find the Plugin class for '" + pluginName + "'");
throw new ExitException(ReturnValue.INVALIDPLUGIN);
} catch (Throwable e) {
e.printStackTrace();
Log.error(e);
throw new ExitException(ReturnValue.FAILURE);
}
if (options.has("help") || options.has("h")) {
Log.stdout(plugin.get_syntax());
} else {
// try to parse the parameters after "--"
plugin.setParams(options.valuesOf(nonOptionSpec));
// pass in the config information from the settings file
plugin.setConfig(config);
// set the metadata object in case the plugin needs access to the DB
plugin.setMetadata(meta);
Log.debug("Setting Up Plugin: " + plugin);
}
} else if (options.has("list")) {
PluginRunner it = new PluginRunner();
String seqwareVersion = it.getClass().getPackage().getImplementationVersion();
Log.stdout("For more information use \"java -jar seqware-distribution-" + seqwareVersion
+ "-full.jar --plugin <plugin_name> --help\" to see options for each.\n");
} else {
getSyntax(parser, "You must specifiy a plugin with option --plugin");
throw new ExitException(ReturnValue.INVALIDARGUMENT);
}
}
/**
* This method invokes each of the plugin methods in turn and evaluates if there are errors.
*
* @return ReturnValue which includes information about status and any errors
*/
private void invokePluginMethods() {
if ((options.has("plugin") || options.has("p")) && plugin != null) {
Log.info("Invoking Plugin Methods:");
// evaluate the plugin method parse_parameters
evaluateReturn(plugin, "parse_parameters");
// evaluate the plugin method init
evaluateReturn(plugin, "init");
// evaluate the plugin method do_test
evaluateReturn(plugin, "do_test");
// evaluate the plugin method do_run
evaluateReturn(plugin, "do_run");
// evaluate the plugin method clean_up
evaluateReturn(plugin, "clean_up");
}
}
/**
* This method calls the specified plugin method and checks for errors.
*
* @param app
* @param methodName
*/
private void evaluateReturn(Plugin app, String methodName) {
Method method;
ReturnValue newReturn = null;
try {
Log.debug(" Invoking Method: " + methodName);
method = app.getClass().getMethod(methodName);
newReturn = (ReturnValue) method.invoke(app);
} catch (Exception e) {
Log.stderr("Module caught exception during method: " + methodName + ":" + e.getMessage());
Log.stderr(ExceptionTools.stackTraceToString(e));
// Exit on error
throw new ExitException(ReturnValue.RUNNERERR);
}
// On failure, update metadb and exit
if (newReturn.getExitStatus() > ReturnValue.SUCCESS) {
if (newReturn.getStderr() != null) {
Log.stderr(newReturn.getStderr());
} else {
Log.stderr("The method '" + methodName + "' exited abnormally so the Runner will terminate here!");
Log.stderr("Return value was: " + newReturn.getExitStatus());
}
throw new ExitException(newReturn.getExitStatus());
}
// Otherwise we will continue, after updating metadata
else {
// If it returned unimplemented, let's warn
if (newReturn.getExitStatus() < ReturnValue.SUCCESS) {
if (!options.has("suppress-unimplemented-warnings")) {
Log.debug("The plugin method '" + methodName + "' returned exit value of " + newReturn.getExitStatus() + ".");
Log.debug("This means an unimplemented method was called (such as an unneeded optional cleanup or init step)");
}
newReturn.setExitStatus(ReturnValue.NULL);
}
}
}
private void setupConfig() {
try {
this.config = ConfigTools.getSettings();
} catch (Exception e) {
Log.stderr("Error reading settings file: " + e.getMessage());
Log.fatal("Error reading settings file", e);
throw new ExitException(ReturnValue.SETTINGSFILENOTFOUND);
}
}
private void setupMetadata() {
this.meta = MetadataFactory.get(config);
}
/**
* The exit code exception is used to communicate exit code values to methods that may wish to call the PluginRunner without running
* into System.exit calls
*/
public static class ExitException extends RuntimeException {
private final int exitCode;
public ExitException(int exitCode) {
this.exitCode = exitCode;
}
/**
* @return the exitCode
*/
public int getExitCode() {
return exitCode;
}
}
}