/* * Copyright 2008 Fedora Commons, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.mulgara.itql; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.io.PrintWriter; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.List; import javax.swing.JFrame; import javax.swing.WindowConstants; import javax.xml.parsers.FactoryConfigurationError; import jline.ConsoleReader; import jline.History; import org.apache.log4j.BasicConfigurator; import org.apache.log4j.Logger; import org.apache.log4j.xml.DOMConfigurator; import org.jrdf.graph.Node; import org.mulgara.query.Answer; import org.mulgara.query.TuplesException; /** * Command line shell for working with TQL sessions. * * @created September 11, 2007 * @author Paula Gearon * @author <a href="mailto:brian@bosatsu.net">Brian Sletten</a> * @copyright © 2007 <a href="http://www.fedora-commons.org/">Fedora Commons</a> */ public class TqlSession { private static final String USER_DIR = "user.dir"; private static final int DEFAULT_HEIGHT = 480; private static final int DEFAULT_WIDTH = 640; public static final String SHELL_NAME = "TQL Shell"; /** Line separator. */ private static final String EOL = System.getProperty("line.separator"); /** The prompt. */ public final static String PS1 = "TQL> "; /** The secondary prompt, indicating an incomplete command. */ public final static String PS2 = "---> "; /** The log4j configuration file path (within the JAR file) */ private final static String LOG4J_CONFIG_PATH = "log4j-tql.xml"; /** The logging category to use */ private final static Logger log = Logger.getLogger(TqlSession.class); /** The default path to the pre-loading script */ private final static String PRE_LOADING_SCRIPT_PATH = "default-pre.tql"; /** The default path to the post-loading script */ private final static String POST_LOADING_SCRIPT_PATH = "default-post.tql"; /** The command prompt */ final static String PROMPT = "TQL> "; /** The graphical UI. */ private TqlSessionUI gui; /** The messages from the previous queries. */ private List<String> messages = new ArrayList<String>(); /** The answers from the previous queries. */ private List<Answer> answers = new ArrayList<Answer>(); /** The file name (URL) of the script to execute if -s is given. */ private String scriptLocation = null; // // Members // /** The TQL auto-interpreter associated with this session */ private final TqlAutoInterpreter autoTql; /** The URL of the post-loading script */ private URL postLoadingScriptUrl = null; /** The URL of the pre-loading script */ private URL preLoadingScriptUrl = null; /** A functor for splitting commands apart. */ private CommandSplitter commandSplitter = null; /** A flag to indicate that an executed command was complete */ private boolean incomplete = false; /** A flag to indicate whether to use the Swing shell or not */ private boolean useSwing = false; /** What machine should we query for models */ private String host = "localhost"; /** The last command that was run. */ private String lastCommand = ""; /** * Start an interactive TQL session from the command prompt. * @param args command line parameters * @throws MalformedURLException Provided URL for a script file is invalid. */ public static void main(String[] args) throws MalformedURLException { // create a new session to work with TqlSession tqlSession = new TqlSession(); try { // set the default pre- and post-loading scripts tqlSession.setDefaultLoadingScripts(); // parse the command line options ItqlOptionParser optsParser = new ItqlOptionParser(args); optsParser.parse(); boolean startSession = tqlSession.handleOptions(optsParser); if (log.isDebugEnabled()) log.debug("Processed command line options"); // execute the pre-loading script - we need to do this after we get the // command line options as we can override the defaults tqlSession.executeLoadingScript(tqlSession.getPreLoadingScriptURL()); if (log.isDebugEnabled()) log.debug("Executed pre-loading script"); // if we can, execute this session using std in and std out if (startSession) tqlSession.runInterface(tqlSession.useSwingShell()); else { // otherwise, run the scripts we were given tqlSession.executeScript(tqlSession.getScript()); tqlSession.executeLoadingScript(tqlSession.getPostLoadingScriptURL()); tqlSession.close(); } } catch (ItqlOptionParser.UnknownOptionException uoe) { errorTermination(tqlSession, "Invalid command line option: " + uoe.getOptionName()); } catch (ItqlOptionParser.IllegalOptionValueException iove) { String optionMsg = "-" + iove.getOption().shortForm() + ", --" + iove.getOption().longForm() + " = " + iove.getValue(); errorTermination(tqlSession, "Invalid command line option value specified: " + optionMsg); } } /** * Convenience method to log errors and terminate the program. * @param session The current session to close. * @param message The error message to log. */ private static void errorTermination(TqlSession session, String message) { log.warn(message); System.err.println(message); session.printUsage(System.out); session.close(); } /** * Constructor. Creates a new TQL session. */ public TqlSession() { // load the default logging configuration this.loadLoggingConfig(); autoTql = new TqlAutoInterpreter(); commandSplitter = new TqlCommandSplitter(); } /** * Returns a list of messages (Strings) from the execution of the last * command or series of commands. Successful and unsuccessful commands will * have valid string objects. * @return all the accumulated messages from the execution of the previous commands. */ List<String> getLastMessages() { return messages; } /** * Returns a list of Answers from the execution of the last command or series * of commands. Failures will not be included. * @return all the accumulated Answers from the execution of the previous commands. */ List<Answer> getLastAnswers() { return answers; } /** * Indicates if the last issued command was complete. If not, then a semicolon was not found * to terminate the command. * @return <code>false</code> only if the last command was complete. <code>true</code> if it completed. */ boolean isCommandIncomplete() { return incomplete; } /** * Indicates if we should use the Swing shell or not */ boolean useSwingShell() { return useSwing; } /** * Returns the host to query for model names */ String getModelHost() { return host; } /** * Executes a series of commands the given command. Accumulates all the * results of these commands into the answers and messages lists. * @param command The command to execute */ void executeCommand(String command) { // Reset answers and messages answers.clear(); messages.clear(); boolean previouslyIncomplete = incomplete; // presume ensuing commands are complete incomplete = false; for (String query: commandSplitter.split(command)) { // clear out empty commands if (incomplete) incomplete = false; // check if we need to follow on if (previouslyIncomplete) { query = lastCommand + query; previouslyIncomplete = false; } lastCommand = query + " "; if (log.isDebugEnabled()) log.debug("Starting execution of command \"" + query + "\""); // execute the command if (!autoTql.executeCommand(query)) { close(); return; } String msg = autoTql.getLastMessage(); if (msg == null) { if (log.isDebugEnabled()) log.debug("Need to follow on for an incomplete command."); incomplete = true; continue; } if (log.isDebugEnabled()) log.debug("Completed execution of commmand \"" + command + "\""); Exception e = autoTql.getLastException(); if (e != null) log.warn("Couldn't execute command", e); // Add the message and answer messages.add(msg); Answer answer = autoTql.getLastAnswer(); if (answer != null) answers.add(answer); } } /** * Executes a script given by URL name. See {@link #executeScript(URL)} for implementation. * @param script The string for the script URL. * @throws MalformedURLException The given script name cannot be represented as a URL. */ private void executeScript(String script) throws MalformedURLException { if (script == null) return; executeScript(new URL(script)); } /** * Executes a script. This is done separately to {@link org.mulgara.query.operation.ExecuteScript} * as it expects to use a single established connection, while this method will establish new * connections for each line, as appropriate. * @param scriptURL the URL of the script to load. May be <code>null</code> in which * case nothing will be done. */ private void executeScript(URL scriptURL) { if (scriptURL == null) return; // log that we're executing the script log.debug("Executing script from " + scriptURL); // keep a record of the line number int line = 0; try { // create a reader to read the contents of the script BufferedReader scriptIn = new BufferedReader(new InputStreamReader(scriptURL.openStream())); String command; while ((command = scriptIn.readLine()) != null) { line++; command = command.trim(); if (!command.equals("")) { autoTql.executeCommand(command); Answer answer = autoTql.getLastAnswer(); if (answer != null) { printAnswer(answer, System.out); answer.close(); } String lastMessage = autoTql.getLastMessage(); if ((lastMessage != null) && !lastMessage.equals("") && (gui != null)) System.out.println(lastMessage); Exception e = autoTql.getLastException(); if (e != null) log.warn("Couldn't execute command", e); } } } catch (TuplesException te) { System.err.println("Error accessing results (line " + line + "): " + te.getMessage()); log.warn("Unable to complete script - " + scriptURL + " (line " + line + ") - " + te); } catch (IOException ioe) { System.err.println("Could not execute script (line " + line + "): " + ioe); log.warn("Unable to execute script - " + scriptURL + " (line " + line + ") - " + ioe); } } private void runInterface(boolean useSwing) { if(useSwing) { // Create the UI. JFrame mainWindow = new JFrame(SHELL_NAME); mainWindow.setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); mainWindow.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); gui = new TqlSessionUI(this, System.in, System.out); mainWindow.getContentPane().add(gui); if (log.isInfoEnabled()) log.info("Starting TQL interpreter"); motdInitialization(); // Start the application, by making the UI visible mainWindow.setVisible(true); } else { try { ConsoleReader reader = new ConsoleReader(); File historyFile = getHistoryFile(); History history = reader.getHistory(); history.setHistoryFile(historyFile); history.setMaxSize(50); reader.setBellEnabled(false); reader.addCompletor(new GraphNameCompletor(prefetchModels(getModelHost()))); String line; PrintWriter out = new PrintWriter(System.out); while ((line = reader.readLine(PS1)) != null) { executeCommand(line); List<Answer> answers = getLastAnswers(); List<String> messages = getLastMessages(); if (answers.isEmpty()) { for (String message: messages) out.println(message); } else { int answerIndex = 0; while (answerIndex < answers.size()) { @SuppressWarnings("unused") String lastMessage = (String)messages.get(answerIndex); try { // Assume the same number of answers and messages Answer answer = answers.get(answerIndex); // If there's more than one answer print a heading. if (answers.size() > 1) { out.println(); // If there's more than one answer add an extra line before the heading. out.println("Executing Query " + (answerIndex+1)); } // print the results if (answer != null) { boolean hasAnswers = true; long rowCount = 0; answer.beforeFirst(); if (answer.isUnconstrained()) { out.println("[ true ]"); rowCount = 1; } else { if (!answer.next()) { out.print("No results returned."); hasAnswers = false; } else { do { rowCount++; out.print("[ "); for (int index = 0; index < answer.getNumberOfVariables(); index++) { Object object = answer.getObject(index); assert(object instanceof Answer) || (object instanceof Node ) || (object == null); out.print(String.valueOf(object)); if (index < (answer.getNumberOfVariables() - 1)) out.print(", "); } out.println(" ]"); } while (answer.next()); } } if (hasAnswers) out.println(rowCount + " rows returned."); answerIndex++; answer.close(); if (line.equalsIgnoreCase("quit") || line.equalsIgnoreCase("exit")) { break; } } } catch(TuplesException te ) { String msg = "Error accessing query results: "; log.warn(msg, te); System.err.println(msg + te.getMessage()); } } } out.flush(); } } catch (IOException e) { String msg = "Error reading from console: "; log.warn(msg, e); System.err.println(msg + e.getMessage()); } } if (log.isInfoEnabled()) log.info("Stopping TQL interpreter"); if (log.isDebugEnabled()) log.debug("Executed post-loading script"); } /* * Get the history file if it exists. * */ private File getHistoryFile() { // TODO: Generalize this process File retValue = new File(System.getProperty("user.home") + File.separator + ".itqllog"); return retValue; } /** * Print the message-of-the-day. */ private void motdInitialization() { gui.print("TQL Command Line Interface" + EOL); gui.print(EOL + "Type \"help ;\", then enter for help." + EOL + EOL + PS1); } /** * Returns the location of the script to run. * @return A string with the location of the script to run. */ private String getScript() throws MalformedURLException { return scriptLocation; } /** * @return the URL of the pre-loading script */ private URL getPreLoadingScriptURL() { return preLoadingScriptUrl; } /** * @return the URL of the post-loading script */ private URL getPostLoadingScriptURL() { return postLoadingScriptUrl; } /** * @return the list of existing model names for tab completion purposes */ private List<String> prefetchModels(String hostname) { List<String> retValue = new ArrayList<String>(); StringBuffer sb = new StringBuffer(); sb.append("select $model from <rmi://"); sb.append(hostname); sb.append("/server1#> where $model $p $o;"); try { executeCommand(sb.toString()); List<Answer> models = getLastAnswers(); for(Answer a : models) { while(a.next()) { retValue.add(a.getObject(0).toString()); } } } catch(Throwable t) { String msg = "Error fetching graph names from server: "; log.error(msg, t); System.err.println(msg + t.getMessage()); } return retValue; } /** * Closes the session associated with this interpreter, and ends the program. * Subclasses that override this method <strong>must</strong> call * <code>super.close()</code>. */ private void close() { // Close the session, if any if (autoTql != null) autoTql.close(); System.exit(0); } /** * Locates and sets the default loading scripts. */ private void setDefaultLoadingScripts() { preLoadingScriptUrl = locateScript(PRE_LOADING_SCRIPT_PATH); postLoadingScriptUrl = locateScript(POST_LOADING_SCRIPT_PATH); } /** * <p>Locates the loading script with the given path.</p> * <p>This locates scripts in the following order:</p> * <ol> * <li> Current working directory;</li> * <li> System classpath (if embedded in a JAR).</li> * </ol> * <p>Note. These could be overwritten by the command-line options <code>-o</code> * and <code>-p</code>.</p> * * @param scriptPath the path to the script to locate * @return a URL to the script, <code>null</code> if the script could not be found */ private URL locateScript(String scriptPath) { URL scriptUrl = null; // find the current directory String currentDirectory = System.getProperty(USER_DIR, "."); // append a "/" if we need to if (!currentDirectory.endsWith("/")) currentDirectory += File.separator; log.debug("Looking for script " + scriptPath + " in " + currentDirectory); // try to find the script File loadingScript = new File(currentDirectory + scriptPath); if (loadingScript.exists() && loadingScript.isFile()) { // found the URL. Return it. log.debug("Found loading script - " + loadingScript); try { scriptUrl = loadingScript.toURI().toURL(); } catch (MalformedURLException mue) { log.warn("Unable to convert loading script filename to URL - " + mue.getMessage()); System.err.println("Unable to convert loading script filename " + "to URL - " + loadingScript); } } else { log.debug("Looking for loading script " + scriptPath + " in classpath"); // try to obtain the URL from the classpath URL loadingScriptUrl = ClassLoader.getSystemResource(scriptPath); if (loadingScriptUrl != null) { log.debug("Found loading script at - " + loadingScriptUrl); scriptUrl = loadingScriptUrl; } } return scriptUrl; } /** * Executes the pre-loading script. * @param loadingScriptUrl the URL of the loading (pre/post) script to execute */ private void executeLoadingScript(URL loadingScriptUrl) { if (loadingScriptUrl != null) { log.debug("Executing loading script " + loadingScriptUrl); executeScript(loadingScriptUrl); } } /** * Processes the command line options passed to the interpreter. * @param parser the command line option parser to use to parse the command line options * @return <code>true</code> if the UI is required, <code>false</code> if the input is a script. */ private boolean handleOptions(ItqlOptionParser parser) { log.debug("Processing command line options"); try { // find out if the user wants help if (parser.getOptionValue(ItqlOptionParser.HELP) != null) { printUsage(System.out); return false; // don't start the UI } else { // dump the interpreter configuration if (null != parser.getOptionValue(ItqlOptionParser.DUMP_CONFIG)) dumpConfig(); String modelHost = (String) parser.getOptionValue(ItqlOptionParser.REMOTE); if(modelHost != null) { host = modelHost; } // load an external interpreter configuration file String itqlConf = (String)parser.getOptionValue(ItqlOptionParser.ITQL_CONFIG); if (itqlConf != null) loadItqlConfig(new URL(itqlConf)); // load an external logging configuration file String logConf = (String)parser.getOptionValue(ItqlOptionParser.LOG_CONFIG); if (logConf != null) loadLoggingConfig(new URL((String)logConf)); // find out whether to execute pre-and post loading scripts if (null == parser.getOptionValue(ItqlOptionParser.NO_LOAD)) { // override the default pre-loading script String preScript = (String)parser.getOptionValue(ItqlOptionParser.PRE_SCRIPT); if (preScript != null) preLoadingScriptUrl = new URL(preScript); // override the default post-loading script String postScript = (String)parser.getOptionValue(ItqlOptionParser.POST_SCRIPT); if (postScript != null) postLoadingScriptUrl = new URL(preScript); // override the default UI environment script useSwing = parser.getOptionValue(ItqlOptionParser.GUI) != null; } else { log.debug("Pre-loading and post-loading scripts disabled"); preLoadingScriptUrl = null; postLoadingScriptUrl = null; } // If there is a script to run, then return false, else true for no script scriptLocation = (String)parser.getOptionValue(ItqlOptionParser.SCRIPT); return null == scriptLocation; } } catch (MalformedURLException e) { log.warn("Invalid URL on command line - " + e.getMessage()); System.err.println("Invalid URL - " + e.getMessage()); printUsage(System.out); } catch (Exception e) { log.warn("Could not start interpreter - " + e.getMessage()); System.err.println("Error - " + e.getMessage()); } // fall through from exception return false; } /** * Prints the usage instructions for the interpreter. * @param out An output stream to print the instructions to. */ private void printUsage(PrintStream out) { // build the usage message StringBuffer usage = new StringBuffer(); usage.append("Usage: java -jar <jarfile> "); usage.append("[-h|-n] "); usage.append("[-l <url>] "); usage.append("[-o <url>] "); usage.append("[-p <url>] "); usage.append("[-s <url>]"); usage.append("[-r <hostname>]"); usage.append(EOL).append(EOL); usage.append("-g, --gui use the Swing shell").append(EOL); usage.append("-h, --help display this help screen").append(EOL); usage.append("-n, --noload do not execute pre- and post-loading ").append(EOL); usage.append("-r, --remote <host> specify a remote host to query for model names").append(EOL); usage.append("scripts (useful with -s)").append(EOL); usage.append("-l, --logconfig use an external logging configuration file").append(EOL); usage.append("-o, --postload execute an TQL script after interpreter stops,").append(EOL); usage.append(" overriding default post-loading script").append(EOL); usage.append("-p, --preload execute an TQL script before interpreter starts,").append(EOL); usage.append(" overriding default pre-loading script").append(EOL); usage.append("-s, --script execute an TQL script and quit").append(EOL); usage.append(EOL); usage.append("The intepreter executes default pre- and post-loading scripts. These can be").append(EOL); usage.append("used to load aliases etc. into the interpreter to simplify commands. The").append(EOL); usage.append("default scripts are contained within the JAR file, however you can overide").append(EOL); usage.append("these by placing files named default-pre.itql and default-post.itql in").append(EOL); usage.append("the directory from which you run the interpreter, or by using the -p and").append(EOL); usage.append("-o options.").append(EOL); // print the usage out.println(usage.toString()); } /** * Dunps the current interpreter configuration to the current directory. This * will dump the entire interpreter configuration including the logging and * application logging. */ private void dumpConfig() { // we don't support this feature yet throw new UnsupportedOperationException(); } /** * Loads an external TQL interpreter configuration file. This will use the * configuration in the file located at <code>itqlConfURL</code>, instead of * the configuration contained within the distribution JAR file. * * @param configUrl the URL of the external iTQL interpreter configuration file * @return <code>true</code> for successful loading of the file. */ private boolean loadItqlConfig(URL configUrl) { // we don't support this feature yet throw new UnsupportedOperationException(); } /** * Loads an external XML log4j configuration file. This will use the * configuration in the file located at <code>logConfURL</code>, instead of * the configuration contained within the distribution JAR file. * @param logConfUrl the URL of the external XML log4j configuration file * @throws Exception if unable to complete the method sucessfully */ private void loadLoggingConfig(URL logConfUrl) throws Exception { // configure the logging service DOMConfigurator.configure(logConfUrl); log.info("Using new logging configuration from " + logConfUrl); } /** * Loads the embedded logging configuration (from the JAR file). */ private void loadLoggingConfig() { // get a URL from the classloader for the logging configuration URL log4jConfigUrl = ClassLoader.getSystemResource(LOG4J_CONFIG_PATH); // if we didn't get a URL, tell the user that something went wrong if (log4jConfigUrl == null) { System.err.println("Unable to find logging configuration file in JAR " + "with " + LOG4J_CONFIG_PATH + ", reverting to default configuration."); BasicConfigurator.configure(); } else { try { // configure the logging service DOMConfigurator.configure(log4jConfigUrl); log.info("Using logging configuration from " + log4jConfigUrl); } catch (FactoryConfigurationError e) { System.err.println("Unable to configure logging service"); } } } /** * Prints an answer to a print stream. * @param answer The answer to print * @param out The print stream to send the answer to. * @throws TuplesException There was an error moving through the data in the answer. */ private void printAnswer(Answer answer, PrintStream out) throws TuplesException { answer.beforeFirst(); if (answer.isUnconstrained()) { out.println("[ true ]"); } else { while (answer.next()) { out.print("[ "); for (int index = 0; index < answer.getNumberOfVariables(); index++) { out.print(String.valueOf(answer.getObject(index))); if (index < (answer.getNumberOfVariables() - 1)) out.print(", "); } out.println(" ]"); } } } }