/* * 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.webquery; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.MissingResourceException; import java.util.Set; import javax.activation.MimeType; import javax.activation.MimeTypeParseException; import javax.mail.BodyPart; import javax.mail.MessagingException; import javax.servlet.ServletConfig; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.mulgara.connection.Connection; import org.mulgara.itql.TqlInterpreter; import org.mulgara.parser.Interpreter; import org.mulgara.parser.MulgaraLexerException; import org.mulgara.parser.MulgaraParserException; import org.mulgara.protocol.http.MimeMultiNamedPart; import org.mulgara.protocol.http.MulgaraServlet; import org.mulgara.protocol.http.ServletDataSource; import org.mulgara.query.Answer; import org.mulgara.query.Query; import org.mulgara.query.QueryException; import org.mulgara.query.TuplesException; import org.mulgara.query.operation.Command; import org.mulgara.query.operation.CreateGraph; import org.mulgara.query.operation.Load; import org.mulgara.server.SessionFactoryProvider; import org.mulgara.sparql.SparqlInterpreter; import org.mulgara.util.ObjectUtil; import org.mulgara.util.SparqlUtil; import org.mulgara.util.StackTrace; import org.mulgara.util.functional.C; import org.mulgara.util.functional.Fn; import org.mulgara.util.functional.Fn1E; import org.mulgara.util.functional.Pair; import static org.mulgara.webquery.Template.*; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE; /** * A web UI for the server. * * @created Jul 28, 2008 * @author Paula Gearon * @copyright © 2008 <a href="http://www.fedora-commons.org/">Fedora Commons</a> */ public class QueryServlet extends MulgaraServlet { /** Serialization by default */ private static final long serialVersionUID = -8407263937557243990L; /** This path is needed to help with variations in different servlet environments. */ public static final String SERVLET_PATH = "/webui"; /** This tutorial path is needed to help with variations in different servlet environments. */ public static final String TUTORIAL_PATH = "/tutorial"; /** Session value for the TQL interpreter. */ private static final String TQL_INTERPRETER = "session.tql.interpreter"; /** Session value for the SPARQL interpreter. */ private static final String SPARQL_INTERPRETER = "session.sparql.interpreter"; /** Posted RDF data content type. */ protected static final String POSTED_DATA_TYPE = "multipart/form-data;"; /** A made-up scheme for data uploaded through http-put, since http means "downloaded". */ protected static final String HTTP_PUT_NS = "http-put://upload/"; /** A string used for debugging purposes. */ protected static final String DEBUG_HOOK = "# backdoor"; /** A string used for debugging purposes. */ protected static final String DEBUG_PAGE = "/example.html"; /** A flag to indicate that debugging needs to be run. */ protected static final boolean DEBUG = true; /** The name of the host for the application. */ private String hostname; /** The name of the server for the application. */ private String servername; /** The default graph URI to use. */ private String defaultGraphUri; /** The path down to the template resource. */ private String templatePath; /** The path prefix for resources. */ private String resourcePath; /** Debugging text. */ private String debugText = ""; /** The path of the base servlet. */ private String basePath = SERVLET_PATH; /** Indicates if this servlet has been initialized. */ private boolean initialized = false; /** * Creates the servlet for the named host. * @param hostname The host name to use, or <code>null</code> if this is not known. * @param servername The name of the current server. * @param server the server */ public QueryServlet(String hostname, String servername, SessionFactoryProvider server) { super(server); init(hostname, servername); } /** * Creates the servlet for the current host. */ public QueryServlet() { initialized = false; } /** * Called by a servlet environment, particularly when this is in a Web ARchive (WAR) file. * @see org.mulgara.protocol.http.MulgaraServlet#init(javax.servlet.ServletConfig) */ public void init(ServletConfig config) { super.init(config); ServletContext context = config.getServletContext(); init(context.getInitParameter(HOST_NAME_PARAM), context.getInitParameter(SERVER_NAME_PARAM)); } /** * Initialize this class with parameters passed from either a Servlet environment, * or a constructing class. * @param hostname The name of this host. If null then the localhost is presumed. * @param servername The name of this service. If null then the default of server1 is used. */ private void init(String hostname, String servername) { if (!initialized) { URL path = ObjectUtil.getClassLoader(this).getResource(ResourceFile.RESOURCES + getTemplateFile()); if (path == null) throw new MissingResourceException("Missing template file", getClass().getName(), ResourceFile.RESOURCES + getTemplateFile()); templatePath = path.toString(); resourcePath = templatePath.split("!")[0]; this.hostname = (hostname != null) ? hostname : DEFAULT_HOSTNAME; this.servername = (servername != null) ? servername : DEFAULT_SERVERNAME; defaultGraphUri = "rmi://" + hostname + "/" + servername + "#sampledata"; initialized = true; } } /** * Respond to a request for the servlet. * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) */ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { String path = req.getRequestURI(); basePath = calcBasePath(path); debugText = path; // case analysis for request type String ext = getExtension(path); if (ext.equals(".jpg") || ext.equals(".png") || ext.equals(".jpeg")) { resp.setContentType("image/jpeg"); new ResourceBinaryFile(relPath(path)).sendTo((OutputStream)resp.getOutputStream()); } else if (ext.equals(".css")) { resp.setContentType("text/css"); new ResourceBinaryFile(relPath(path)).sendTo(resp.getOutputStream()); } else { // file request resp.setContentType("text/html"); resp.setHeader("pragma", "no-cache"); // check for some parameters String resultOrdinal = req.getParameter(RESULT_ORD_ARG); String queryGetGraph = req.getParameter(GRAPH_ARG); // return the appropriate page for the given parameters if (resultOrdinal != null) { doNextPage(req, resp, resultOrdinal); } else if (queryGetGraph != null) { doQuery(req, resp, queryGetGraph); } else { clearOldResults(req); outputStandardTemplate(resp); } } } /** * Respond to a request for the servlet. * @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) */ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { basePath = calcBasePath(req.getRequestURI()); if (!req.getRequestURI().contains("/" + EXECUTE_LINK)) { resp.sendError(SC_BAD_REQUEST, "Sent a command to the wrong page."); return; } String type = req.getContentType(); if (type != null && type.startsWith(POSTED_DATA_TYPE)) handleDataUpload(req, resp); else doQuery(req, resp, req.getParameter(GRAPH_ARG)); } /** * Provide a description for the servlet. * @see javax.servlet.GenericServlet#getServletInfo() */ public String getServletInfo() { return "Mulgara Query UI"; } /** * Generates the standard page from the template HTML file. * @param resp The object used to respond to the client. * @throws IOException Caused by an error writing to the client. */ private void outputStandardTemplate(HttpServletResponse resp) throws IOException { PrintWriter out = resp.getWriter(); new ResourceTextFile(getTemplateFile(), getTemplateTags()).sendTo(out); out.close(); } /** * Generates a debugging page. * @param resp The object used to respond to the client. * @throws IOException Caused by an error writing to the client. */ private void outputFile(HttpServletResponse resp, String filename) throws IOException { PrintWriter out = resp.getWriter(); new ResourceTextFile(filename).sendTo(out); out.close(); } /** * Execute the appropriate query, and display the results. * @param req The user request. * @param resp The response object for output to the user. * @param graphUri The URI in a user request. * @throws IOException Error sending a response to the client. */ private void doQuery(HttpServletRequest req, HttpServletResponse resp, String graphUri) throws IOException { clearOldResults(req); // work out which commands to run String command = generateCommand(req, graphUri); if (DEBUG && command != null && command.contains(DEBUG_HOOK)) { outputFile(resp, DEBUG_PAGE); return; } // No command to run, so show the entry page if (command == null || command.length() == 0) { outputStandardTemplate(resp); return; } // execute all the commands, and accumulate the results List<Object> results = null; List<Command> cmds = null; long time = 0; try { // record how long this takes time = System.currentTimeMillis(); final Connection c = getConnection(req); cmds = getInterpreter(req, command, graphUri).parseCommands(command); results = C.map(cmds, new Fn1E<Command,Object,Exception>() { public Object fn(Command cmd) throws Exception { return cmd.execute(c); } }); time = System.currentTimeMillis() - time; } catch (MulgaraParserException mpe) { resp.sendError(SC_BAD_REQUEST, "Error parsing command: " + mpe.getMessage()); return; } catch (RequestException re) { resp.sendError(SC_BAD_REQUEST, "Error processing request: " + re.getMessage()); return; } catch (IllegalStateException ise) { resp.sendError(SC_SERVICE_UNAVAILABLE, ise.getMessage()); return; } catch (Exception e) { // resp.sendError(SC_BAD_REQUEST, "Error executing command. Reason: " + StackTrace.getReasonMessage(e)); resp.sendError(SC_BAD_REQUEST, "Error executing command. Reason: " + StackTrace.throwableToString(e)); return; } // Use the first graph mentioned as the future default for (Command c: cmds) { if (c instanceof Query) { updateDefaultGraph(c); break; } } // Get the tags to use in the page template Map<String,String> templateTags = getTemplateTagMap(); templateTags.put(GRAPH_TAG, defaultGraph(graphUri)); // Generate the page QueryResponsePage page = new QueryResponsePage(req, resp, templateTags, getTemplateHeaderFile(), getTemplateTailFile()); page.writeResults(time, cmds, results); } /** * Print out the next page of a set of results, specified by the ordinal number * @param req The request environment. * @param resp The response object. * @param ordinalStr The result number to display the next page for. * @throws IOException Error responding to the client. */ private void doNextPage(HttpServletRequest req, HttpServletResponse resp, String ordinalStr) throws IOException { // get the session/request parameters, and validate Map<Answer,Pair<Long,Command>> unfinishedResults = getUnfinishedResults(req); int ordinal = 0; try { ordinal = Integer.parseInt(ordinalStr); } catch (NumberFormatException nfe) { ordinal = -1; } if (ordinal <= 0 || unfinishedResults == null || ordinal > unfinishedResults.size()) { resp.sendError(SC_BAD_REQUEST, "Result not available. Did you use the \"Back button\"? (result " + ordinalStr + " of " + ((unfinishedResults == null) ? 0 : unfinishedResults.size()) + ")"); clearOldResults(req); return; } // Close and remove all the results we don't need Answer remaining = closeExcept(unfinishedResults.keySet(), ordinal); // Get the tags to use in the page template Map<String,String> templateTags = getTemplateTagMap(); templateTags.put(GRAPH_TAG, defaultGraph(req.getParameter(GRAPH_ARG))); // Generate the page QueryResponsePage page = new QueryResponsePage(req, resp, templateTags, getTemplateHeaderFile(), getTemplateTailFile()); page.writeResult(unfinishedResults.get(remaining).second(), remaining); } /** * Do the work of extracting data to be uploaded, and put it in the requested graph. Create the graph if needed. * @param req The HTTP request containing the file. * @param resp The response back to the submitting client. * @throws IOException Due to an error reading the input stream, or writing to the response stream. */ private void handleDataUpload(HttpServletRequest req, HttpServletResponse resp) throws IOException { try { // parse in the data to be uploaded MimeMultiNamedPart mime = new MimeMultiNamedPart(new ServletDataSource(req, UPLOAD_GRAPH_ARG)); // validate the request if (mime.getCount() == 0) throw new RequestException("Request claims to have posted data, but none was supplied."); long time = System.currentTimeMillis(); // Get the destination graph, and ensure it exists URI destGraph = getRequestedGraph(req, mime); Connection conn = getConnection(req); try { new CreateGraph(destGraph).execute(conn); } catch (QueryException e) { throw new RequestException("Unable to create graph <" + destGraph + ">: " + e.getMessage()); } // upload the data Command loadCommand = null; for (int partNr = 0; partNr < mime.getCount(); partNr++) { BodyPart part = mime.getBodyPart(partNr); String partName = mime.getPartName(partNr); try { if (UPLOAD_FILE_ARG.equalsIgnoreCase(partName)) { loadCommand = loadData(destGraph, part, conn); break; } } catch (QueryException e) { String filename = part.getFileName(); String message = "Unable to load data: " + (filename == null ? partName : filename); message += ". " + StackTrace.getReasonMessage(e); throw new RequestException(message); } } time = System.currentTimeMillis() - time; // Get the tags to use in the page template Map<String,String> templateTags = getTemplateTagMap(); templateTags.put(GRAPH_TAG, defaultGraph(destGraph.toString())); // Generate the page QueryResponsePage page = new QueryResponsePage(req, resp, templateTags, getTemplateHeaderFile(), getTemplateTailFile()); page.writeResults(time, Collections.singletonList(loadCommand), Collections.singletonList((Object)"")); } catch (MessagingException e) { resp.sendError(SC_BAD_REQUEST, "Unable to process received MIME data: " + e.getMessage()); } catch (RequestException re) { resp.sendError(SC_BAD_REQUEST, re.getMessage()); } } /** * Gets the graph parameter from the MIME data. * @param req The request object from the user. * @return The URI for the requested graph. * @throws RequestException If a graph name was an invalid URI or was not present in the request. */ protected URI getRequestedGraph(HttpServletRequest req, MimeMultiNamedPart mime) throws RequestException { // look in the parameters String[] graphArgs = req.getParameterValues(UPLOAD_GRAPH_ARG); if (graphArgs != null) { if (graphArgs.length != 1) throw new RequestException("Multiple graphs requested."); try { return new URI(graphArgs[0]); } catch (URISyntaxException e) { throw new RequestException("Invalid URI for upload graph. " + e.getInput()); } } // look in the mime data if (mime != null) { try { String result = mime.getParameterString(UPLOAD_GRAPH_ARG); if (result != null) { try { return new URI(result); } catch (URISyntaxException e) { throw new RequestException("Invalid URI for upload graph <" + e.getInput() + ">: " + result); } } } catch (Exception e) { throw new RequestException("Bad MIME data in upload request: " + e.getMessage()); } } throw new RequestException("No graph argument provided."); } /** * Load MIME data into a graph. * @param graph The graph to load into. * @param data The data to be loaded, with associated meta-data. * @param cxt The connection to the database. * @return The Command that did the loading. * @throws IOException error reading from the client. * @throws RequestException Bad data passed to the load request. * @throws QueryException A query exception occurred during the load operation. */ protected Load loadData(URI graph, BodyPart data, Connection cxt) throws RequestException, IOException, QueryException { String contentType = ""; try { contentType = data.getContentType(); Load loadCmd = new Load(graph, data.getInputStream(), new MimeType(contentType), data.getFileName()); loadCmd.execute(cxt); return loadCmd; } catch (MessagingException e) { throw new RequestException("Unable to process data for loading: " + e.getMessage()); } catch (MimeTypeParseException e) { throw new RequestException("Bad Content Type in request: " + contentType + " (" + e.getMessage() + ")"); } } /** * Close all but one answer. * @param answers The answers to close. * @param ordinal The ordinal (1-based) of the answer to NOT close. * @return The Answer that did NOT get removed. */ private Answer closeExcept(Set<Answer> answers, int ordinal) { Answer excludedResult = null; Iterator<Answer> i = answers.iterator(); int nrResults = answers.size(); for (int r = 0; r < nrResults; r++) { assert i.hasNext(); if (r == ordinal - 1) { excludedResult = i.next(); } else { try { i.next().close(); } catch (TuplesException e) { /* ignore */ } i.remove(); } } assert !i.hasNext(); assert excludedResult != null; return excludedResult; } /** * Clears out any old results found in the session. * @param req The current environment. */ private void clearOldResults(HttpServletRequest req) { Map<Answer,Pair<Long,Command>> results = getUnfinishedResults(req); try { if (results != null) { for (Answer a: results.keySet()) a.close(); results.clear(); } } catch (TuplesException e) { // ignoring these problems, since the answer is being thrown away } } /** * Finds any unfinished data in the current session. * @param req The current request environment. * @return The unfinished results that were recorded in this session, or <code>null</code> * if there were no unfinished results. */ @SuppressWarnings("unchecked") private Map<Answer,Pair<Long,Command>> getUnfinishedResults(HttpServletRequest req) { Map<Answer,Pair<Long,Command>> oldResultData = (Map<Answer,Pair<Long,Command>>)req.getSession().getAttribute(UNFINISHED_RESULTS); return (oldResultData == null) ? null : oldResultData; } /** * Analyse the request parameters and work out what kind of query to generate. * Resource and Literal queries are mutually exclusive, and both override * an explicit query argument. * @param req The request environment. * @param graphUri The graphUri set for this request. * @return The command or commands to execute. */ private String generateCommand(HttpServletRequest req, String graphUri) { String queryResource = req.getParameter(QUERY_RESOURCE_ARG); if (queryResource != null) return buildResourceQuery(graphUri, queryResource); String queryLiteral = req.getParameter(QUERY_LITERAL_ARG); if (queryLiteral != null) return buildLiteralQuery(graphUri, queryLiteral); return req.getParameter(QUERY_TEXT_ARG); } /** * Get a query string to display a resource. * @param graph The name of the graph to get the resource from. * @param resource The name of the resource. * @return The query string. */ private String buildResourceQuery(String graph, String resource) { StringBuilder urlString = new StringBuilder("select $Predicate $Object from <"); urlString.append(graph).append("> where <").append(resource).append("> $Predicate $Object;"); urlString.append("select $Subject $Predicate from <").append(graph); urlString.append("> where $Subject $Predicate <").append(resource).append(">;"); urlString.append("select $Subject $Object from <").append(graph); urlString.append("> where $Subject <").append(resource).append("> $Object;"); return urlString.toString(); } /** * Get a query string to display a literal. * @param graph The name of the graph to get the resource from. * @param literal The value of the literal. * @return The query string. */ private String buildLiteralQuery(String graph, String literal) { return "select $Subject $Predicate from <" + graph + "> where $Subject $Predicate " + literal + ";"; } /** * Takes each of the template tags and creates a map out of them. * @return A map of all tags to the data to replace them. */ private Map<String,String> getTemplateTagMap() { Map<String,String> tagMap = new HashMap<String,String>(); String[][] source = getTemplateTags(); for (String[] tag: source) tagMap.put(tag[0], tag[1]); return tagMap; } /** * Gets the list of tags to be replaced in a template document, along with the values * to replace them with. * @return An array of string pairs. The first string in the pair is the tag to replace, * the second string is the value to repace the tag with. */ private String[][] getTemplateTags() { return new String[][] { new String[] {HOSTNAME_TAG, hostname}, new String[] {SERVERNAME_TAG, servername}, new String[] {JARURL_TAG, resourcePath}, new String[] {EXECUTE_TAG, EXECUTE_LINK}, new String[] {DEBUG_TAG, debugText}, new String[] {BASE_PATH_TAG, basePath} }; } /** * Updates the default graph URI to one found in a Query. * @param cmd A Command that contains a query. */ private void updateDefaultGraph(Command cmd) { assert cmd instanceof Query; Set<URI> graphs = ((Query)cmd).getModelExpression().getGraphURIs(); if (!graphs.isEmpty()) defaultGraphUri = C.first(graphs).toString(); } /** * Creates the default graph name for the sample data. * @param graphParam The graph name that the user has already set. * @return The default graph name to use when no graph has been set. */ private String defaultGraph(String graphParam) { if (graphParam != null && graphParam.length() > 0) return graphParam; return defaultGraphUri; } /** * Gets the interpreter for the current session, creating it if it doesn't exist yet. * @param req The current request environment. * @param cmd The command the interpreter will be used on. * @param graphUri The string form of the URI for the default graph to use in the interpreter. * @return A connection that is tied to this HTTP session. * @throws RequestException An internal error occured with the default graph URI. */ private Interpreter getInterpreter(HttpServletRequest req, String cmd, String graphUri) throws RequestException { RegInterpreter ri = getRegInterpreter(cmd); HttpSession httpSession = req.getSession(); Interpreter interpreter = (Interpreter)httpSession.getAttribute(ri.getRegString()); if (interpreter == null) { interpreter = ri.getInterpreterFactory().fn(); httpSession.setAttribute(ri.getRegString(), interpreter); } setDefaultGraph(interpreter, graphUri); return interpreter; } /** * Sets the default graph on an interpreter * @param i The interpreter to set the default graph for. * @param graph The graph to use with the interpreter. * @throws RequestException An internal error where a valid graph could not be refered to. */ private void setDefaultGraph(Interpreter i, String graph) throws RequestException { // set the default graph, if applicable try { i.setDefaultGraphUri(graph); } catch (Exception e) { try { i.setDefaultGraphUri(defaultGraphUri); } catch (URISyntaxException e1) { throw new RequestException("Unable to create URI for: " + defaultGraphUri, e1); } } } /** * Gets a factory for creating an interpreter, along with the name for that type of interpreter * to be registered under * @param query The query to determine the interpreter type * @return An interpreter constructor and name */ private RegInterpreter getRegInterpreter(String query) { Fn<Interpreter> factory = null; String attr = null; if (SparqlUtil.looksLikeSparql(query)) { factory = new Fn<Interpreter>(){ public Interpreter fn(){ return new SparqlInterpreter(); }}; attr = SPARQL_INTERPRETER; } else { factory = new Fn<Interpreter>(){ public Interpreter fn() { return new TerminatingTqlInterpreter(); }}; attr = TQL_INTERPRETER; } return new RegInterpreter(factory, attr); } /** * Get the name of the file to be used for the template. * @return The absolute file path, with a root set at the resource directory. */ protected String getTemplateFile() { return TEMPLATE; } /** * Get the name of the file to be used for the header template. * @return The absolute file path, with a root set at the resource directory. */ protected String getTemplateHeaderFile() { return TEMPLATE_HEAD; } /** * Get the name of the file to be used for the footer template. * @return The absolute file path, with a root set at the resource directory. */ protected String getTemplateTailFile() { return TEMPLATE_TAIL; } /** * Compare a parameter name to a set of known parameter names used for uploading. * @param name The name to check. * @return <code>true</code> if the name is known. <code>false</code> if not known or <code>null</code>. */ @SuppressWarnings("unused") private boolean knownUploadParam(String name) { final String[] knownParams = new String[] { UPLOAD_GRAPH_ARG }; for (String p: knownParams) if (p.equalsIgnoreCase(name)) return true; return false; } /** * Returns the filename extension for a given path. * @param path The path to get the extension for. * @return The extension, including the . character. If there is no extension, then an empty string. */ private static String getExtension(String path) { if (path == null) return ""; int dot = path.lastIndexOf('.'); if (dot < 0) return ""; return path.substring(dot); } private String calcBasePath(String fullpath) { if (fullpath.contains(SERVLET_PATH)) { return fullpath.substring(0, fullpath.indexOf(SERVLET_PATH) + SERVLET_PATH.length()) + "/"; } if (fullpath.contains(TUTORIAL_PATH)) { return fullpath.substring(0, fullpath.indexOf(TUTORIAL_PATH) + TUTORIAL_PATH.length()) + "/"; } return "/"; } /** * Returns a relative path, starting from a given base. * @param full The full path to be truncated. * @return The new relative path. */ private String relPath(String full) { if (full.startsWith(basePath)) { String path = full.substring(basePath.length()); return path.startsWith("/") ? path : "/" + path; } return full; } /** * Registerable Interpreter. This contains a factory for an interpreter, plus the name it should * be registered under. */ private static class RegInterpreter { /** The interpreter factory */ private final Fn<Interpreter> intFactory; /** The registration name for the interpreter built from the factory */ private final String regString; /** Create a link between an interpreter factory and the name it should be registered under */ public RegInterpreter(Fn<Interpreter> intFactory, String regString) { this.intFactory = intFactory; this.regString = regString; } /** Get the method for creating an interpreter */ public Fn<Interpreter> getInterpreterFactory() { return intFactory; } /** Get the name constructed interpreters should be created under */ public String getRegString() { return regString; } } /** * Extension of TQL interpreter that will automatically terminate any * commands that are not already terminated. */ private static class TerminatingTqlInterpreter extends TqlInterpreter { /** The terminating character. */ private static final String TERMINATOR = ";"; /** * Calls TqlInterpreter#parseCommands(String) with a guaranteed termination of ";". * @see TqlInterpreter#parseCommands(String) */ public List<Command> parseCommands(String command) throws MulgaraParserException, MulgaraLexerException, IOException { command = command.trim(); if (!command.endsWith(TERMINATOR)) command += TERMINATOR; return super.parseCommands(command); } } }