/* * 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; // Java 2 standard packages import java.io.*; import java.net.*; import java.util.*; // Third party packages import org.apache.log4j.Logger; // Apache Log4J import org.jrdf.graph.*; // JRDF // Locally written packages // Automatically generated packages (SableCC) import org.mulgara.itql.analysis.*; import org.mulgara.itql.lexer.*; import org.mulgara.itql.node.*; import org.mulgara.itql.parser.*; import org.mulgara.query.operation.AddAlias; import org.mulgara.query.operation.ApplyRules; import org.mulgara.query.operation.Backup; import org.mulgara.query.operation.Command; import org.mulgara.query.operation.Commit; import org.mulgara.query.operation.CreateGraph; import org.mulgara.query.operation.Deletion; import org.mulgara.query.operation.DropGraph; import org.mulgara.query.operation.ExecuteScript; import org.mulgara.query.operation.Export; import org.mulgara.query.operation.Help; import org.mulgara.query.operation.Insertion; import org.mulgara.query.operation.ListAlias; import org.mulgara.query.operation.Load; import org.mulgara.query.operation.Modification; import org.mulgara.query.operation.Quit; import org.mulgara.query.operation.Restore; import org.mulgara.query.operation.Rollback; import org.mulgara.query.operation.SetAutoCommit; import org.mulgara.query.operation.SetTime; import org.mulgara.query.operation.SetUser; import org.mulgara.parser.Interpreter; import org.mulgara.parser.MulgaraLexerException; import org.mulgara.parser.MulgaraParserException; import org.mulgara.query.*; import org.mulgara.query.rdf.*; import org.mulgara.server.Session; import org.mulgara.util.ServerInfoRef; import org.mulgara.util.URIUtil; /** * Interactive TQL (ITQL) command interpreter. * Performs parsing and converting TQL requests to query objects for execution; * Based on ItqlInterpreter. * * <em>This class is non-reentrant. Parsing should be serialized, or else use a new TqlInterpreters * for each thread.</em> * * @created 2007-08-09 * @author Paula Gearon * @copyright ©2007 <a href="http://www.fedora-commons.org/">Fedora Commons</a> */ public class TqlInterpreter extends DepthFirstAdapter implements SableCCInterpreter, Aliasing { /** The logger */ static final Logger logger = Logger.getLogger(TqlInterpreter.class.getName()); static { // force initialization of static, unsynchronized variables inside these classes new Parser(new Lexer2()); } /** A constraint expression builder. */ private ConstraintExpressionBuilder builder = new ConstraintExpressionBuilder(this); /** Variable factory for this interpreter. */ private VariableFactory variableFactory = new VariableFactoryImpl(); /** Lexer... */ Lexer2 lexer = new Lexer2(); // // Members // /** The map from targets to aliases */ private Map<String,URI> aliasMap = null; /** The log file to record all iTQL requests */ private PrintWriter itqlLog = null; /** The location of the log iTQL file */ private String itqlLogFile = null; /** The command for the callbacks to fill, while parseCommand is running */ Command lastCommand = null; /** The last exception or error, to be filled in during the callback operations. */ Throwable lastError = null; // // Interpreter options // /** The next anonymous variable suffix. */ private int anonSuffix = 0; // // Constructors // /** * Creates a new ITQL command interpreter, using the default alias set. */ public TqlInterpreter() { this(getDefaultAliases()); } /** * Creates a new ITQL command interpreter. * * @param aliasMap the map from targets to aliases, never <code>null</code> */ public TqlInterpreter(Map<String,URI> aliasMap) { // validate aliasMap parameter if (aliasMap == null) throw new IllegalArgumentException("Null \"alias\" parameter"); // set members setAliasMap(aliasMap); // log the creation of this interpreter if (logger.isDebugEnabled()) { logger.debug("Itql interpreter created"); } // is this session configured for logging. if (System.getProperty("itql.command.log") != null) { itqlLogFile = System.getProperty("itql.command.log"); logger.info("iTQL command logging has been enabled. Logging to " + System.getProperty("itql.command.log")); } } /** * Set up default aliases. * * @return A map of aliases to their fully qualified names */ public static Map<String,URI> getDefaultAliases() { Map<String,URI> aliases = new HashMap<String,URI>(); aliases.put(RDF, URI.create(RDF_NS)); aliases.put(RDFS, URI.create(RDFS_NS)); aliases.put(OWL, URI.create(OWL_NS)); aliases.put(MULGARA, URI.create(MULGARA_NS)); aliases.put(KRULE, URI.create(KRULE_NS)); aliases.put(DC, URI.create(DC_NS)); aliases.put(SKOS, URI.create(SKOS_NS)); aliases.put(FOAF, URI.create(FOAF_NS)); return aliases; } // // Public API // /** * Parses the given TQL command. * * @param command the command to parse in TQL syntax * @return An AST for the command * @throws MulgaraParserException if the syntax of the command is incorrect * @throws LexerException if the syntax of the command is incorrect * @throws IOException if the <var>command</var> cannot be parsed * @throws IllegalArgumentException if the <var>command</var> is <code>null</code> */ public Command parseCommand(String command) throws MulgaraParserException, MulgaraLexerException, IOException { // validate command parameter if ((command == null) || command.equals("")) { throw new IllegalArgumentException("Null \"command\" parameter"); } // log that we're going to execute the command if (logger.isDebugEnabled()) logger.debug("Parsing command " + command); resetInterpreter(); // log the iTQL command - system property itql.command.log must be set // log the command abd push it into the lexer this.logItql(command); try { lexer.add(command); } catch (LexerException le) { flush(); throw new MulgaraLexerException(le.getMessage(), le); } // test that this is a single command if (lexer.getCommandCount() > 1) logger.warn("Multiple commands given to parser"); try { // if the lexer saw terminators, parse the associated commands if (lexer.nextCommand()) { Start commandTree = null; String commandText = lexer.getCurrentCommand(); // parse the command Parser parser = new Parser(lexer); commandTree = parser.parse(); // Build the command. This populates lastCommand commandTree.apply(this); lastCommand.setText(commandText); if (logger.isDebugEnabled()) logger.debug("Successfully parsed command " + command); } } catch (LexerException le) { throw new MulgaraLexerException(le); } catch (ParserException pe) { throw new MulgaraParserException(pe); } finally { flush(); } return lastCommand; } /** * Parses the given TQL command. * * @param command the command to parse in TQL syntax * @return A {@link List} of ASTs, one for each command * @throws MulgaraParserException if the syntax of the command is incorrect * @throws LexerException if the syntax of the command is incorrect * @throws IOException if the <var>command</var> cannot be paersed * @throws IllegalArgumentException if the <var>command</var> is <code>null</code> */ public List<Command> parseCommands(String command) throws MulgaraParserException, MulgaraLexerException, IOException { // validate command parameter if ((command == null) || command.equals("")) { throw new IllegalArgumentException("Null \"command\" parameter"); } // log that we're going to execute the command if (logger.isDebugEnabled()) logger.debug("Parsing command " + command); // log the iTQL command - system property itql.command.log must be set this.logItql(command); // clean up command list command = command.trim(); if (!command.endsWith(";")) command = command + ";"; // Reset the variable incrementer in the query. variableFactory.reset(); // push the command into the lexer try { lexer.add(command); } catch (LexerException le) { flush(); throw new MulgaraLexerException(le); } // create a list of AST versions of the command List<Command> commandList = new LinkedList<Command>(); // if the lexer saw terminators, parse the associated commands while (lexer.nextCommand()) { String commandText = lexer.getCurrentCommand(); Start commandTree = null; // parse the command try { Parser parser = new Parser(lexer); commandTree = parser.parse(); // build the command // this populates lastCommand resetInterpreter(); commandTree.apply(this); if (lastCommand != null) { lastCommand.setText(commandText); // take the lastCommand result, and add it to the list of results commandList.add(lastCommand); if (logger.isDebugEnabled()) logger.debug("Successfully parsed command: " + command); } else { if (logger.isDebugEnabled()) logger.debug("Null parse: " + command); } } catch (ParserException e) { flush(); throw new MulgaraParserException(e); } catch (LexerException e) { flush(); throw new MulgaraLexerException(e); } catch (Error e) { flush(); throw e; } } return commandList; } /** * Parse a string into a {@link Query}. Convenience method over parseCommand. * * @param queryString a string containing an ITQL query * @return the corresponding {@link Query} instance * @throws IOException if <var>queryString</var> can't be buffered. * @throws LexerException if <var>queryString</var> can't be tokenized. * @throws MulgaraParserException if <var>queryString</var> is not syntactic. */ public Query parseQuery(String queryString) throws IOException, MulgaraLexerException, MulgaraParserException { if (queryString == null) throw new IllegalArgumentException("Null \"queryString\" parameter"); // clean up query queryString = queryString.trim(); while (queryString.endsWith(";")) { queryString = queryString.substring(0, queryString.length() - 1); } // log that we're going to execute the command if (logger.isDebugEnabled()) logger.debug("Parsing query \"" + queryString + "\""); // parse the command via double dispatch Parser parser = new Parser(new Lexer(new PushbackReader(new StringReader(queryString), 256))); resetInterpreter(); try { parser.parse().apply(this); } catch (LexerException le) { throw new MulgaraLexerException(le); } catch (ParserException pe) { throw new MulgaraParserException(pe); } // should now have the command parsed into lastCommand. // check the the command worked as expected if (lastCommand == null) throw new MulgaraParserException("Parameter was not a query"); if (!(lastCommand instanceof Query)) throw new IllegalArgumentException("Command was not a query: " + queryString); // This may not be accurate if there was more than one query, but if the lexer stopped short, we're stuck here. lastCommand.setText(queryString); // return the results of the command return (Query)lastCommand; } // // Methods overridden from DepthFirstAdapter // Provides callback mechanism for SableCC // /** * Displays help information to the user. * * @param node the help command */ public void outAHelpCommand(AHelpCommand node) { // log the command if (logger.isDebugEnabled()) logger.debug("Processing help command " + node); lastCommand = new Help(HelpPrinter.getHelp(node.getCommandPrefix())); } /** * Quits a session. * * @param node the quit command */ public void outAQuitCommand(AQuitCommand node) { // log the command if (logger.isDebugEnabled()) logger.debug("Processing quit command " + node); lastCommand = new Quit(); } /** * Commits a transaction. * * @param node the commit command */ public void outACommitCommand(ACommitCommand node) { // log the command if (logger.isDebugEnabled()) logger.debug("Processing commit command " + node); lastCommand = new Commit(); } /** * Rolls back a transaction. * * @param node the rollback command */ public void outARollbackCommand(ARollbackCommand node) { // log the command if (logger.isDebugEnabled()) logger.debug("Processing rollback command " + node); lastCommand = new Rollback(); } /** * Creates a query. * * @param node the query command */ public void outASelectCommand(ASelectCommand node) { // log the command if (logger.isDebugEnabled()) logger.debug("Processing select command " + node); resetInterpreter(); // build the query try { lastCommand = this.buildQuery(node.getQuery()); } catch (QueryException qe) { logger.warn("Couldn't answer query", qe); lastError = qe; } catch (URISyntaxException use) { logger.warn("Invalid resource URI. " + use.getMessage()); lastError = use; } } /** * Substitutes the user associated with this session. * * @param node the su command */ public void outASuCommand(ASuCommand node) { // log the command if (logger.isDebugEnabled()) logger.debug("Processing su command " + node); lastCommand = new SetUser(node.getUser().getText(), node.getPassword().getText(), toURI(node.getResource())); } /** * Associates an alias prefix with a target. * * @param node the alias command */ public void outAAliasCommand(AAliasCommand node) { // log the command if (logger.isDebugEnabled()) logger.debug("Processing alias command " + node); // get the prefix and target String aliasPrefix = node.getPrefix().getText(); String aliasTarget = node.getTarget().getText(); try { // convert the target to a URI URI aliasTargetURI = new URI(aliasTarget); // log the conversion if (logger.isDebugEnabled()) logger.debug("Converted " + aliasTarget + " to URI " + aliasTargetURI); // add the alias pair to the map this.addAliasPair(aliasPrefix, aliasTargetURI); // log that we've added the pair to the map if (logger.isDebugEnabled()) logger.debug("Aliased " + aliasTarget + " as " + aliasPrefix); // Return an AST element, for reporting on what happened. lastCommand = new AddAlias(aliasPrefix, aliasTarget); } catch (URISyntaxException use) { // log the failed URI creation logger.warn("Unable to create URI from alias target " + aliasTarget); } } /** * Requests a list of current aliases. * * @param node the alias command */ public void outAAliaslCommand(AAliaslCommand node) { // log the command if (logger.isDebugEnabled()) logger.debug("Processing alias list command " + node); // Return an AST element, for reporting on what happened. // Use a Help command, with the alias listing as the help text. lastCommand = new ListAlias(Collections.unmodifiableMap(aliasMap)); } /** * Applies a set of rules in a model to data in another model. * * @param node the alias command */ public void outAApplyCommand(AApplyCommand node) { // log the command if (logger.isDebugEnabled()) logger.debug("Processing apply command " + node); try { // get the rule graph and target graph URI ruleGraph = toURI(node.getRules()); PModelExpression rawModelExpression = node.getBase(); GraphExpression baseGraph = GraphExpressionBuilder.build(aliasMap, rawModelExpression); Token dest = null; PDestinationClause rawDestinationClause = node.getDestination(); if (rawDestinationClause != null) { dest = ((ADestinationClause)rawDestinationClause).getResource(); } URI destGraph = null; if (dest == null) { destGraph = baseGraph instanceof GraphResource ? ((GraphResource)baseGraph).getURI() : URI.create(Mulgara.DEFAULT_GRAPH); } else { destGraph = toURI(dest); } lastCommand = new ApplyRules(ruleGraph, baseGraph, destGraph); } catch (QueryException qe) { logger.warn("Couldn't apply rules", qe); lastError = qe; } catch (URISyntaxException use) { logger.warn("Invalid resource URI. " + use.getMessage()); lastError = use; } } /** * Creates a new database/model. * * @param node the create command */ public void outACreateCommand(ACreateCommand node) { // log the command if (logger.isDebugEnabled()) logger.debug("Processing create command " + node); // get the name of the model to create URI graphURI = toURI(node.getModel()); // get the type of model to create; default to mulgara:Graph is unspecified URI graphTypeURI = (node.getModelType() == null) ? Session.MULGARA_GRAPH_URI : toURI(node.getModelType()); // log that we're asking the driver to create the resource if (logger.isDebugEnabled()) logger.debug("Creating new graph " + graphURI); graphURI = getCanonicalUriAlias(graphURI); if (logger.isDebugEnabled()) logger.debug("Graph is alias for " + graphURI); lastCommand = new CreateGraph(graphURI, graphTypeURI); } /** * Drop (delete) a database/model. * * @param node the drop command */ public void outADropCommand(ADropCommand node) { // log the command if (logger.isDebugEnabled()) logger.debug("Processing drop command " + node); // get the name of the database/model to drop lastCommand = new DropGraph(toURI(node.getResource())); } /** * Load the contents of a file into a database/model. * * @param node the load command */ public void outALoadCommand(ALoadCommand node) { // log the command if (logger.isDebugEnabled()) logger.debug("Processing load command " + node); // get constituents of the load command URI sourceURI = toURI(node.getSource()); URI destinationURI = toURI(node.getDestination()); boolean locality = node.getLocality() != null && (node.getLocality() instanceof ALocalLocality); lastCommand = new Load(sourceURI, destinationURI, locality); } /** * Executes a TQL script. * * @param node the execute command */ public void outAExecuteCommand(AExecuteCommand node) { // log the command if (logger.isDebugEnabled()) logger.debug("Processing execute command " + node); // get the name of the script to execute String resource = node.getResource().getText(); try { lastCommand = new ExecuteScript(new URL(resource), this); } catch (MalformedURLException mue) { // let the user know the problem logger.warn("Invalid script source URL: " + resource); lastError = mue; } } /** * Inserts a triple, model, database or the results of a query into a model or * database. * * @param node the insert command */ public void outAInsertCommand(AInsertCommand node) { // log the command if (logger.isDebugEnabled()) logger.debug("Processing insert command " + node); // get the resource we're inserting data into URI graphURI = toURI(node.getResource()); lastCommand = buildModification(graphURI, node.getTripleFactor(), true); } /** * Deletes a triple, model, database or the results of a query from a model or * database. * * @param node the delete command */ public void outADeleteCommand(ADeleteCommand node) { // log the command if (logger.isDebugEnabled()) logger.debug("Processing delete command " + node); // get the resource we're inserting data into URI graphURI = toURI(node.getResource()); lastCommand = buildModification(graphURI, node.getTripleFactor(), false); } /** * Sets an interpreter property. * * @param node the set command */ public void outASetCommand(ASetCommand node) { // log the command if (logger.isDebugEnabled()) logger.debug("Processing set command " + node); // get the option to set PSetOption option = node.getSetOption(); // log that we've got the option if (logger.isDebugEnabled()) logger.debug("Found option " + option); // get the value boolean optionSet = !(node.getSetOptionMode() instanceof AOffSetOptionMode); // set the option if (option instanceof ATimeSetOption) { if (logger.isDebugEnabled()) logger.debug("Found set time: " + (optionSet ? "on" : "off")); lastCommand = new SetTime(optionSet); } else if (option instanceof AAutocommitSetOption) { if (logger.isDebugEnabled()) logger.debug("Found autocommit: " + (optionSet ? "on" : "off")); lastCommand = new SetAutoCommit(optionSet); } else { lastError = new ItqlInterpreterException("Unknown interpreter option for \"SET\""); } } /** * Backs up the contents of a server to a local or remote file. * * @param node the backup command */ @SuppressWarnings("deprecation") public void outABackupCommand(ABackupCommand node) { // log the command if (logger.isDebugEnabled()) logger.debug("Processing backup command " + node); // get constituents of the backup command URI destinationURI = toURI(node.getDestination()); boolean locality = node.getLocality() != null && (node.getLocality() instanceof ALocalLocality); Token src = node.getSource(); if (src != null) { URI sourceURI = toURI(src); lastCommand = new Backup(sourceURI, destinationURI, locality); } else { lastCommand = new Backup(destinationURI, locality); } } /** * Exports the contents of a graph to a local or remote file. * * @param node the backup command */ public void outAExportCommand(AExportCommand node) { // log the command if (logger.isDebugEnabled()) logger.debug("Processing export command " + node); // get constituents of the export command URI sourceURI = toURI(node.getSource()); URI destinationURI = toURI(node.getDestination()); boolean locality = node.getLocality() != null && (node.getLocality() instanceof ALocalLocality); Export exportCommand = new Export(sourceURI, destinationURI, locality); exportCommand.setNamespacePrefixes(aliasMap); lastCommand = exportCommand; } /** * Restores the contents of a server from a file. * * @param node the restore command */ @SuppressWarnings("deprecation") public void outARestoreCommand(ARestoreCommand node) { // log the command if (logger.isDebugEnabled()) logger.debug("Processing restore command " + node); URI sourceURI = toURI(node.getSource()); boolean locality = node.getLocality() != null && (node.getLocality() instanceof ALocalLocality); Token dest = node.getDestination(); if (dest != null) { URI destinationURI = toURI(dest); lastCommand = new Restore(sourceURI, destinationURI, locality); } else { lastCommand = new Restore(sourceURI, locality); } } /** * Returns a set of statements from the iTQL query object. * * @param setOfTriples the set of statements defined in the query. * @param variableMap the variable map to store the value of the variable * against the variable object. * @throws URISyntaxException if <code>tripleFactor</code> contains a query or * a resource that that violates <a * href="http://www.isi.edu/in-notes/rfc2396.txt">RFC\uFFFD2396</a> * @throws QueryException if an invalid node is used in the set of triples. * @return a set of statements from the iTQL query. */ @SuppressWarnings("unchecked") public Set<org.jrdf.graph.Triple> getStatements(ATripleSetOfTriples setOfTriples, Map<String,VariableNodeImpl> variableMap) throws QueryException, URISyntaxException { List<ATriple> tripleList = (List<ATriple>)setOfTriples.getTriple(); HashSet<org.jrdf.graph.Triple> statements = new HashSet<org.jrdf.graph.Triple>(); // Check that each set of triples has the predicate bound. for (Iterator<ATriple> i = tripleList.iterator(); i.hasNext(); ) { // get the triple ATriple triple = i.next(); // Convert the Subject, Predicate and Object. org.jrdf.graph.Node subject = toNode(triple.getSubject(), variableMap); org.jrdf.graph.Node predicate = toNode(triple.getPredicate(), variableMap); org.jrdf.graph.Node object = toNode(triple.getObject(), variableMap); // Predicate cannot be a blank node. if (predicate instanceof BlankNode) { throw new QueryException("Predicate must be a valid URI"); } // Check that the subject or predicate node is not a literal. if (subject instanceof LiteralImpl || predicate instanceof LiteralImpl) { // throw an exception indicating we have a bad triple throw new QueryException( "Subject or Predicate cannot be a literal"); } // Create a new statement using the triple elements org.jrdf.graph.Triple jrdfTriple = new TripleImpl( (SubjectNode) subject, (PredicateNode) predicate, (ObjectNode) object); // add the statement to the statement set statements.add(jrdfTriple); } return statements; } /** * Creates a query from iTQL syntax and grammar. * Despite onyl being accessed from within this package (both locally and from * {@link VariableBuilder}) this method must be public, as it has to appear in * the {@link Interpreter} interface. * * @param rawQuery a select query, represented as either a {@link * org.mulgara.itql.node.ASelectCommand} or a {@link * org.mulgara.itql.node.ASelectSetOfTriples} * @return the answer to the query * @throws QueryException if the query cannot be executed * @throws URISyntaxException if the <code>query</code> contains a resource * whose text violates <a href="http://www.isi.edu/in-notes/rfc2396.txt"> * RFC\uFFFD2396</a> */ @SuppressWarnings("unchecked") public Query buildQuery(org.mulgara.itql.node.Node rawQuery) throws QueryException, URISyntaxException { // validate query parameter if (rawQuery == null) throw new IllegalArgumentException("Null \"rawQuery\" parameter"); // create the variables. May contain a PElement; Count; URI literal; or a sub query LinkedList<PElement> variables = null; AFromClause fromClause; AWhereClause whereClause; AOrderClause orderClause; AHavingClause havingClause; ALimitClause limitClause; AOffsetClause offsetClause; boolean distinct = true; // cast the correct way (we don't have a common superclass, event though we // have methods with the same names) if (rawQuery instanceof AQuery) { AQuery query = (AQuery) rawQuery; PSelectClause selectClause = query.getSelectClause(); if (selectClause instanceof ANormalSelectSelectClause) { distinct = ((ANormalSelectSelectClause)selectClause).getNondistinct() == null; variables = (LinkedList<PElement>)((ANormalSelectSelectClause)selectClause).getElement(); } fromClause = ((AFromClause)query.getFromClause()); whereClause = ((AWhereClause)query.getWhereClause()); orderClause = ((AOrderClause)query.getOrderClause()); havingClause = ((AHavingClause)query.getHavingClause()); limitClause = ((ALimitClause)query.getLimitClause()); offsetClause = ((AOffsetClause)query.getOffsetClause()); } else if (rawQuery instanceof ASelectSetOfTriples) { ASelectSetOfTriples query = (ASelectSetOfTriples) rawQuery; variables = new LinkedList<PElement>(); variables.add(query.getSubject()); variables.add(query.getPredicate()); variables.add(query.getObject()); fromClause = ((AFromClause)query.getFromClause()); whereClause = ((AWhereClause)query.getWhereClause()); orderClause = ((AOrderClause)query.getOrderClause()); havingClause = ((AHavingClause)query.getHavingClause()); limitClause = ((ALimitClause)query.getLimitClause()); offsetClause = ((AOffsetClause)query.getOffsetClause()); } else { // we only handle AQuery and ASelectSetOfTriples throw new IllegalArgumentException("Invalid type for \"rawQuery\" parameter"); } if (fromClause == null) throw new QueryException("FROM clause missing."); if (whereClause == null) throw new QueryException("WHERE clause missing."); // build the variable list: collection of Variable, ConstantValue, Count, Subquery if (logger.isDebugEnabled()) logger.debug("Building query variable list from " + variables); List<SelectElement> variableList = this.buildVariableList(variables); if (logger.isDebugEnabled()) logger.debug("Built variable list " + variableList); // get the model expression from the parser PModelExpression rawModelExpression = fromClause.getModelExpression(); if (logger.isDebugEnabled()) logger.debug("Building model expression from " + rawModelExpression); // parse the text into a model expression GraphExpression graphExpression = GraphExpressionBuilder.build(this.getAliasMap(), rawModelExpression); if (logger.isDebugEnabled()) logger.debug("Built model expression " + graphExpression); // get the constraint expression from the parser PConstraintExpression rawConstraintExpression = whereClause.getConstraintExpression(); if (logger.isDebugEnabled()) logger.debug("Building constraint expression from " + rawConstraintExpression); // parse the text into a constraint expression ConstraintExpression constraintExpression = build(rawConstraintExpression); if (logger.isDebugEnabled()) logger.debug("Built constraint expression " + constraintExpression); // build the order list List<Order> orderList = buildOrderList(orderClause); // build the having clause ConstraintHaving havingExpression = buildHaving(havingClause); // build the limit and offset Integer limit = null; int offset = 0; try { if (limitClause != null) limit = new Integer(limitClause.getNumber().getText()); if (offsetClause != null) offset = Integer.parseInt(offsetClause.getNumber().getText()); } catch (NumberFormatException e) { String failedType = (limit == null) ? "limit" : "offset"; throw new Error("Parser permitted non-integer for " + failedType, e); } // build a query using the information we've obtained from the parser return new Query(variableList, graphExpression, constraintExpression, havingExpression, orderList, limit, offset, distinct, new UnconstrainedAnswer()); } /** * Despite onyl being accessed from within this package (both locally and from * {@link VariableBuilder}) this method must be public, as it has to appear in * the {@link Interpreter} interface. * * @see org.mulgara.itql.SableCCInterpreter#toLiteralImpl(PLiteral) */ public LiteralImpl toLiteralImpl(PLiteral p) { ALiteral aLiteral = (ALiteral)p; // Determine the datatype URI, if present ADatatype type = (ADatatype)aLiteral.getDatatype(); URI datatypeURI = (type != null) ? toURI(type.getResource()) : null; if (datatypeURI != null) { return new LiteralImpl(getLiteralText(aLiteral), datatypeURI); } else { // Determine the language code String language = toLanguageId((ALanguage)aLiteral.getLanguage()); if (language != null) return new LiteralImpl(getLiteralText(aLiteral), language); // no language, so return an untyped, no language literal return new LiteralImpl(getLiteralText(aLiteral)); } } /** * @see org.mulgara.itql.SableCCInterpreter#toURI(Token) */ public URI toURI(Token token) { assert token instanceof TResource; return URIUtil.convertToURI(token.getText(), aliasMap); } /** * Called by {@link ConstraintExpressionBuilder} * @see org.mulgara.itql.SableCCInterpreter#nextAnonVariable() */ public Variable nextAnonVariable() { return new Variable("av__" + this.anonSuffix++); } /** * Builds a {@link org.mulgara.query.ConstraintExpression} object from a * {@link org.mulgara.itql.node.PConstraintExpression}, using an <code>aliasMap</code> * to resolve aliases. Uses double-dispatch into the AST for the expression. * * @param expression a constraint expression from the parser * @return A new constraint expression, based on the AST passed as "expression". * @throws QueryException if <code>rawConstraintExpression</code> does not * represent a valid query * @throws URISyntaxException if the <code>rawConstraintExpression</code> * contains a resource whose text violates <a * href="http://www.isi.edu/in-notes/rfc2396.txt">RFC?2396</a> */ public ConstraintExpression build(PConstraintExpression expression) throws QueryException, URISyntaxException { // validate parameters if (aliasMap == null) throw new IllegalArgumentException("Null \"aliasMap\" parameter"); if (expression == null) throw new IllegalArgumentException("Null \"expression\" parameter"); if (logger.isDebugEnabled()) logger.debug("Building constraint expression from " + expression); // build the contraint expression from the parser input expression.apply((Switch)builder); ConstraintExpression constraintExpression = builder.getConstraintExpression(); if (logger.isDebugEnabled()) logger.debug("Successfully built constraint expression from " + expression); // return the new constraint expression return constraintExpression; } /** * Returns the text of the given <code>literal</code>. * * @param literal the literal to retrieve the text from * @return The LiteralText value */ @SuppressWarnings("unchecked") public static String getLiteralText(ALiteral literal) { // validate the literal parameter if (literal == null) throw new IllegalArgumentException("Null \"literal\" " + "parameter"); // the text of the literal StringBuffer literalText = new StringBuffer(); // get all the strands in this literal List<PStrand> strands = (List<PStrand>)literal.getStrand(); // add each strand together to make the literal text for (PStrand strand: strands) { // add the strand to the literal text if (strand instanceof AUnescapedStrand) { literalText.append(((AUnescapedStrand)strand).getText().getText()); } else if (strand instanceof AEscapedStrand) { literalText.append(((AEscapedStrand)strand).getEscapedtext().getText()); } } return literalText.toString(); } /** * Sets the alias map associated with this interpreter. * * @param aliasMap the alias map associated with this interpreter */ public void setAliasMap(Map<String,URI> aliasMap) { this.aliasMap = aliasMap; } /** * Returns the alias map associated with this session. * * @return the alias namespace map associated with this session */ public Map<String,URI> getAliasMap() { return aliasMap; } /** @see org.mulgara.parser.Interpreter#setDefaultGraphUri(java.lang.String) */ public Interpreter setDefaultGraphUri(String graph) throws URISyntaxException { return this; } /** @see org.mulgara.parser.Interpreter#setDefaultGraphUri(java.net.URI) */ public Interpreter setDefaultGraphUri(URI graph) { return this; } /** * Builds a list of {@link org.mulgara.query.Variable}s from a list of * {@link org.mulgara.itql.node.PVariable}s. Note. Variables in both the * <code>rawVariableList</code> and the returned list will <strong>not * </strong> contain the variable prefix <code>$</code> in their name. * * @param rawVariableList a list of {@link * org.mulgara.itql.node.PVariable}s from the parser * @return a list of {@link org.mulgara.query.Variable}s, suitable for use * in creating a {@link org.mulgara.query.Query} * @throws QueryException if the <code>rawVariableList</code> cannot be parsed * into a list of {@link org.mulgara.query.Variable}s */ List<SelectElement> buildVariableList(LinkedList<PElement> rawVariableList) throws QueryException, URISyntaxException { // Empty variable list. if (rawVariableList == null) return Collections.emptyList(); // validate rawVariableList parameter if (rawVariableList.size() == 0) throw new IllegalArgumentException("Empty \"rawVariableList\" parameter"); // Construct the required builder VariableBuilder variableBuilder = new VariableBuilder(this, variableFactory); // end if // log that we're building the variable list if (logger.isDebugEnabled()) logger.debug("Building variable list from " + rawVariableList); // copy each variable from the query into the list for (PElement element: rawVariableList) element.apply((Switch)variableBuilder); // Get the variable list List<SelectElement> variableList = variableBuilder.getVariableList(); // make sure that we return a list with something in it if (variableList.size() == 0) { throw new QueryException("No variables parseable from query"); } // log that we've successfully built the variable list if (logger.isDebugEnabled()) { logger.debug("Built variable list " + variableList); } // return the list return variableList; } /** * Builds a list of {@link org.mulgara.query.Variable}s from a list of * {@link org.mulgara.itql.node.POrderElement}s. Note. Variables in both * the <code>rawVariableList</code> and the returned list will <strong>not * </strong> contain the variable prefix <code>$</code> in their name. * * @param orderClause The SableCC list of elements to order by. * @return a list of {@link org.mulgara.query.Variable}s, suitable for use * in creating a {@link org.mulgara.query.Query}, or an empty list if * there are no elements to be ordered by. * @throws QueryException if the <code>rawOrderElementList</code> cannot be * parsed into a list of {@link org.mulgara.query.Variable}s */ @SuppressWarnings("unchecked") List<Order> buildOrderList(AOrderClause orderClause) throws QueryException { // short circuit for an empty clause if (orderClause == null) return (List<Order>)Collections.EMPTY_LIST; // get the list of elements in the clause LinkedList<AOrderElement> rawOrderList = (LinkedList<AOrderElement>)orderClause.getOrderElement(); assert rawOrderList != null && !rawOrderList.isEmpty(); if (logger.isDebugEnabled()) logger.debug("Building order list from " + rawOrderList); // create a list for the parsed variables List<Order> orderList = new ArrayList<Order>(rawOrderList.size()); // copy each variable from the query into the list for (AOrderElement order: rawOrderList) { // get the name of this variable String variableName = ((AVariable)order.getVariable()).getIdentifier().getText(); if (logger.isDebugEnabled()) logger.debug("Found variable $" + variableName); // Figure out which way to order, ascending or descending boolean ascending; PDirection direction = order.getDirection(); if (direction == null) { ascending = true; } else if (direction instanceof AAscendingDirection) { ascending = true; } else if (direction instanceof ADescendingDirection) { ascending = false; } else { throw new Error("Unknown direction field in order"); } // add a new ordered variable to the list orderList.add(new Order(new Variable(variableName), ascending)); } // make sure that we return a list with something in it if (orderList.size() == 0) throw new QueryException("No variables parseable from query"); if (logger.isDebugEnabled()) logger.debug("Built order list " + orderList); return orderList; } /** * Builds a HAVING compliant {@link org.mulgara.query.ConstraintExpression} object from a * {@link org.mulgara.itql.node.PConstraintExpression}, using an <code>aliasMap</code> * to resolve aliases. To comply with a HAVING clause the predicate must be one of: * mulgara:occurs mulgara:occursLessThan mulgara:occursMoreThan. * * @param havingClause a constraint expression from the parser * @return A new ConstraintExpression representing the HAVING condition * @throws QueryException if <code>rawConstraintExpression</code> does not * represent a valid query * @throws URISyntaxException if the <code>rawConstraintExpression</code> * contains a resource whose text violates <a * href="http://www.isi.edu/in-notes/rfc2396.txt">RFC?2396</a> */ ConstraintHaving buildHaving(AHavingClause havingClause) throws QueryException, URISyntaxException { // short circuit if there is no having clause if (havingClause == null) return null; // get the constraint expression from the parser PConstraintExpression rawHavingExpression = havingClause.getConstraintExpression(); if (logger.isDebugEnabled()) logger.debug("Building constraint expression from " + rawHavingExpression); ConstraintExpression hExpr = build(rawHavingExpression); // do some gramatical checking on the clause if (hExpr instanceof ConstraintOperation) throw new QueryException("Having currently supports only one constraint"); if (!checkHavingPredicates(hExpr)) throw new QueryException("Only \"occurs\" predicates can be used in a Having clause"); return (ConstraintHaving)hExpr; } /** * Convert a literal's language node into a language ID * @param language The node containing the language node. * @return The 2 or 5 character identifier, or <code>null</code> if no code available. * @throws QueryException if the ID of the language is malformed. */ private static String toLanguageId(ALanguage language) { if (language == null) return null; String langId = language.getLangid().getText(); int len = langId.length(); if (len != 2 && len != 5) { logger.error("Unknown form for language tag: " + langId); langId = null; } return langId; } /** * Resets the parser state in preparation for a new command. */ private void resetInterpreter() { lastCommand = null; lastError = null; // Reset the variable incrementer in the query. variableFactory.reset(); } /** * @param graphURI * @param tripleFactor */ private Modification buildModification(URI graphURI, PTripleFactor tripleFactor, boolean asserting) { // get the set of triples out of the factor PSetOfTriples setOfTriples = null; if (tripleFactor instanceof ABracedTripleFactor) { setOfTriples = ((ABracedTripleFactor)tripleFactor).getSetOfTriples(); } else if (tripleFactor instanceof AUnbracedTripleFactor) { setOfTriples = ((AUnbracedTripleFactor)tripleFactor).getSetOfTriples(); } else throw new RuntimeException("Unhandled Grammar Exception: Unknown type of triple factor: " + tripleFactor.getClass().getName()); try { // Create the correct type of modifier for the data if (setOfTriples instanceof AResourceSetOfTriples) { // this is an insert of one model into another. throw new UnsupportedOperationException("No support for direct model to model insertion."); } else if (setOfTriples instanceof ASelectSetOfTriples) { // This is an INSERT/SELECT // build the query Query query = this.buildQuery((ASelectSetOfTriples)setOfTriples); if (logger.isDebugEnabled()) logger.debug("Insert query " + query); return newModifier(graphURI, query, asserting); } else if (setOfTriples instanceof ATripleSetOfTriples) { // This is an inline set of triples Set<Triple> statements = getStatements((ATripleSetOfTriples)setOfTriples, new HashMap<String,VariableNodeImpl>()); return newModifier(graphURI, statements, asserting); } } catch (URISyntaxException ue) { logger.warn("Invalid URL in the insertion data: " + ue.getMessage()); lastError = ue; } catch (QueryException qe) { logger.warn("Bad query for insertion: " + qe.getMessage()); lastError = qe; } return null; } /** * Factory method to create a Modification object. * @param graphURI The URI of the graph to be modified. * @param query The query to select the data to be modified. * @param asserting Indicates if the data needs to be asserted (inserted) * or denied (deleted). * @return An {@link Insertion} if asserting is <code>true</code>, * otherwise a {@link Deletion}. */ private Modification newModifier(URI graphURI, Query query, boolean asserting) { return asserting ? new Insertion(graphURI, query) : new Deletion(graphURI, query); } /** * Factory method to create a Modification object. * @param graphURI The URI of the graph to be modified. * @param statements A set of triples to be modified. * @param asserting Indicates if the data needs to be asserted (inserted) * or denied (deleted). * @return An {@link Insertion} if asserting is <code>true</code>, * otherwise a {@link Deletion}. */ private Modification newModifier(URI graphURI, Set<Triple> statements, boolean asserting) { return asserting ? new Insertion(graphURI, statements) : new Deletion(graphURI, statements); } /** * Constructs a {@link org.jrdf.graph.Node} from a {@link * org.mulgara.itql.node.PTripleElement}. * * @param element dd * @param variableMap a {@link Map} of variable names (as string) to * {@link VariableNodeImpl} that are used to contain all variables. * @return dd * @throws QueryException if <code>element</code> is a {@link * org.mulgara.itql.node.AResourceTripleElement} whose text contains a * <a href="http://www.w3.org/TR/REC-xml-names/#ns-qualnames">qualified * name</a> with a prefix not defined in the <code>aliasMap</code> * @throws URISyntaxException if <code>element</code> is a {@link * org.mulgara.itql.node.AResourceTripleElement} whose text doesn't * conform to <a href="http://www.isi.edu/in-notes/rfc2396.txt"> * RFC\uFFFD2396</a> */ private org.jrdf.graph.Node toNode(PTripleElement element, Map<String,VariableNodeImpl> variableMap) throws QueryException, URISyntaxException { // validate the element parameter if (element == null) throw new IllegalArgumentException("Null \"element\" parameter"); if (logger.isDebugEnabled()) logger.debug("Resolving " + element + "to a RDF node"); // create the node org.jrdf.graph.Node node = null; // get the node if (element instanceof ALiteralTripleElement) { // create a new literal with the given text node = toLiteralImpl(((ALiteralTripleElement)element).getLiteral()); } else if (element instanceof AResourceTripleElement) { // create a new resource node = new URIReferenceImpl(toURI(((AResourceTripleElement)element).getResource()), false); } else if (element instanceof AVariableTripleElement) { // get the variable String variableName = ((AVariable)((AVariableTripleElement)element).getVariable()).getIdentifier().getText(); if (logger.isDebugEnabled()) logger.debug("Resolved " + element + " to variable " + variableName); // use a map to keep the same variable objects if they can be reused if (variableMap.containsKey(variableName)) { node = (VariableNodeImpl)variableMap.get(variableName); } else { node = new VariableNodeImpl(variableName); variableMap.put(variableName, (VariableNodeImpl)node); } } // return the node return node; } /** * Adds a name/value pair to the alias map. This method will add associate a * prefix for a target for subsequent commands, making commands like the * following possible: <PRE> * alias http://purl.org/dc/elements/1.1 as dc; * select $title where $uri dc:title $title ; * </PRE> * * @param aliasPrefix the alias that denotes the target * @param aliasTarget the target associated with the prefix */ private void addAliasPair(String aliasPrefix, URI aliasTarget) { // validate the parameters if (aliasPrefix == null) throw new IllegalArgumentException("Null \"aliasPrefix\" " + "parameter"); if (aliasTarget == null) throw new IllegalArgumentException("Null \"aliasTarget\" " + "parameter"); // add the pair to the map getAliasMap().put(aliasPrefix, aliasTarget); } /** * Log the TQL command to a specified file * * @param command The TQL command to be validated */ private void logItql(String command) { // Short circuit if not logging. The constructor initialises this if // system property itql.command.log is set if (itqlLogFile == null) return; try { // open log if needed if (itqlLog == null) itqlLog = new PrintWriter(new FileWriter(itqlLogFile, true), true); // append the command to the file itqlLog.println(command); } catch (Exception ex) { logger.error("Unable to log itql commands", ex); } } /** * Discard any unparsed tokens. */ private void flush() { lexer.leftoverTokenList.clear(); } /** * Checks that all predicates in a constraint expression are valid Having predicates * from {@link SpecialPredicates}. * * @param e The constraint expression to check. * @return true if all constraints have special predicates. */ private boolean checkHavingPredicates(ConstraintExpression e) { if (e instanceof Constraint) { return e instanceof ConstraintHaving; } else if (e instanceof ConstraintOperation) { // check all sub expressions for (ConstraintExpression expr: ((ConstraintOperation)e).getElements()) { if (checkHavingPredicates(expr)) return false; } // all sub expressions returned true return true; } else { // An unexpected type return false; } } /** Local constants list of supported protocols. */ private static final Set<String> protocols = new HashSet<String>(); static { protocols.add("rmi"); protocols.add("soap"); } /** * Try to recognise a uri alias, and return the canonical form instead. * * @param uri The URI being checked. * @return The updated URI. May be the same as the uri parameter. */ private URI getCanonicalUriAlias(URI uri) { // only do this for remote protocols if (!protocols.contains(uri.getScheme())) return uri; logger.debug("Checking for an alias on: " + uri); // extract the host name String host = uri.getHost(); if (host == null) return uri; Set<String> hostnames = ServerInfoRef.getHostnameAliases(); // Check with a DNS server to see if this host is recognised InetAddress addr = null; try { addr = InetAddress.getByName(host); } catch (UnknownHostException uhe) { // The host was unknown, so allow resolution to continue as before return uri; } // check the various names against known aliases and the given name if ( hostnames.contains(host) || hostnames.contains(addr.getHostName()) || hostnames.contains(addr.getCanonicalHostName()) || hostnames.contains(addr.getHostAddress()) ) { // change the host name to one that is recognised // use the system uri to find the local host name URI serverURI = ServerInfoRef.getServerURI(); if (serverURI == null) { return uri; } String newHost = serverURI.getHost(); try { return new URI(uri.getScheme(), newHost, uri.getPath(), uri.getFragment()); } catch (URISyntaxException e) { /* fall through */ } } // not found, so return nothing return uri; } private static class Lexer2 extends Lexer { int commandCount = 0; final LinkedList<Token> leftoverTokenList = new LinkedList<Token>(); StringBuilder buildingCommand = new StringBuilder(); LinkedList<String> commandQueue = new LinkedList<String>(); String currentCommand = null; public Lexer2() { super(null); } public int getCommandCount() { return commandCount; } public void add(String command) throws LexerException, IOException { Lexer lexer = new Lexer(new PushbackReader(new StringReader(command), 256)); Token t; while (!((t = lexer.next()) instanceof EOF)) { if (t instanceof TTerminator) { commandQueue.addLast(buildingCommand.toString()); buildingCommand = new StringBuilder(); t = new EOF(); commandCount++; assert commandCount == commandQueue.size(); } else { buildingCommand.append(t.getText()); } leftoverTokenList.add(t); } } public Token next() throws LexerException, IOException { return leftoverTokenList.isEmpty() ? new EOF() : (Token) leftoverTokenList.removeFirst(); } public Token peek() throws LexerException, IOException { return leftoverTokenList.isEmpty() ? new EOF() : (Token) leftoverTokenList.getFirst(); } public boolean nextCommand() { if (commandCount == 0) { return false; } else { //assert commandCount > 0; commandCount--; currentCommand = commandQueue.remove(); assert commandCount == commandQueue.size(); return true; } } public String getCurrentCommand() { return currentCommand; } } }