/*
* 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.net.URI;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.log4j.Logger;
import org.mulgara.connection.Connection;
import org.mulgara.connection.ConnectionException;
import org.mulgara.connection.ConnectionFactory;
import org.mulgara.connection.DummyConnection;
import org.mulgara.parser.Interpreter;
import org.mulgara.query.Answer;
import org.mulgara.query.QueryException;
import org.mulgara.query.operation.Command;
import org.mulgara.query.operation.Commit;
import org.mulgara.query.operation.LocalCommand;
import org.mulgara.query.operation.Rollback;
import org.mulgara.query.operation.SetAutoCommit;
import org.mulgara.query.operation.TxOp;
import org.mulgara.server.Session;
/**
* This class interprets TQL statements, and automatically executes them,
* establishing connections to servers when required.
*
* @created Sep 11, 2007
* @author Paula Gearon
* @copyright © 2007 <a href="http://www.fedora-commons.org/">Fedora Commons</a>
*/
public class TqlAutoInterpreter {
/** The logger. */
private final static Logger logger = Logger.getLogger(TqlAutoInterpreter.class.getName());
/** A connection for receiving state changes to the local machine. */
private static Connection localStateConnection = new DummyConnection();
/** The parser and AST builder for commands. */
private Interpreter interpreter = new TqlInterpreter();
/** A user readable message resulting from the most recent command. */
private String lastMessage;
/** The most answer returned from the most recent command, if it was a query. */
private Answer lastAnswer;
/** The most recent exception, if there was one. */
private Exception lastException;
/** Factory for building and caching connections. */
private ConnectionFactory connectionFactory = new ConnectionFactory();
/** Indicates that the client is in a transaction. */
private boolean inTransaction;
/** All the connections involved in the current transaction. */
private Map<URI,Connection> transConnections = new HashMap<URI,Connection>();
/**
* Holds the client security domain. Need to connect this to URIs,
* but the old interfaces don't know how to do this.
* <em>Security is currently unimplemented.</em>
*/
private URI securityDomain = null;
/**
* Creates a new autointerpreter with no prior connections.
*/
public TqlAutoInterpreter() {
inTransaction = false;
resetState();
}
/**
* Execute a query. The results of the query will set the state of this object.
* @param command The string containing the query to execute.
* @return <code>false</code> if the command asks to exit, <code>true</code> to continue normal operation.
*/
public boolean executeCommand(String command) {
resetState();
if (logger.isDebugEnabled()) logger.debug("Parsing the command: " + command);
Command cmd = null;
try {
cmd = interpreter.parseCommand(command);
} catch (Exception e) {
lastMessage = "Error parsing the query";
lastException = e;
return true;
}
if (cmd == null) {
lastMessage = null;
return true;
}
// execute the operation
try {
// set up a connection, if required
Connection conn = establishConnection(cmd);
handleResult(cmd.execute(conn), cmd);
updateConnectionsForTx(conn, cmd);
lastMessage = cmd.getResultMessage();
} catch (Exception e) {
lastException = e;
lastMessage = "Error: " + e.getMessage();
}
assert lastMessage != null;
// test if the command wants the user to quit - return false if it does
return !(cmd.isLocalOperation() && ((LocalCommand)cmd).isQuitCommand());
}
/**
* Sets the security domain for the client.
* <em>Security is currently unimplemented.</em>
* @param domain The URI of the service which authenticates the client.
* e.g. ldap://ldap.domain.net/o=mycompany
*/
public void setSecurityDomain(URI domain) {
securityDomain = domain;
}
/**
* Query for the currently used security domain.
* <em>Security is currently unimplemented.</em>
*/
public URI getSecurityDomain() {
return securityDomain;
}
/** @return the message set from the last operation */
public String getLastMessage() { return lastMessage; }
/** @return the last answer returned from a query, or <code>null</code> if the last operation was not a query */
public Answer getLastAnswer() { return lastAnswer; }
/** @return the exception thrown from the last operation, or <code>null</code> if there was no error */
public Exception getLastException() { return lastException; }
/**
* Close any resources that are still in use, and rolls back any outstanding transactions.
*/
public void close() {
if (inTransaction) {
logger.info("Closing a current transaction. Rolling back.");
try {
handleTxOp(new SetAutoCommit(true));
} catch (QueryException e) {
logger.error("Error while cleaning up a transaction", e);
}
}
assert transConnections.isEmpty();
connectionFactory.closeAll();
}
/**
* Resets the internal state in preparation for a new operation to be executed.
*/
private void resetState() {
lastMessage = null;
lastAnswer = null;
lastException = null;
}
/**
* Process the result from a command.
* @param result The result to handle.
* @param cmd The command that gave the result. Used for type checking.
*/
private void handleResult(Object result, Command cmd) {
if (result != null) {
if (cmd.isAnswerable()) lastAnswer = (Answer)result;
else logger.debug("Result: " + result);
}
}
/**
* Returns a connection to a server for a given command.
* @param cmd The command to get a connection to execute on.
* @return A connection to the server, cached if available.
* @throws ConnectionException It was not possible to create a connection to the described server.
* @throws QueryException There is a transaction underway, but the new connection cannot turn off autocommit.
*/
private Connection establishConnection(Command cmd) throws ConnectionException, QueryException {
URI serverUri = cmd.getServerURI();
// check for server operations where we don't know the server
if (serverUri == null && !cmd.isLocalOperation()) {
// no server URI, but not local. Get a connection for a null URI
// eg. select .... from <file:///...>
Connection connection = transConnections.get(serverUri);
if (connection == null) {
connection = connectionFactory.newConnection(serverUri);
configureForTransaction(serverUri, connection);
}
return connection;
}
// go the normal route for getting a connection for a given server location
return establishConnection(serverUri);
}
/**
* Returns a connection to the server with the given URI.
* NB: Not for general use. Available to ItqlInterpreterBean only to support
* legacy requests to get a session.
* @param serverUri The URI for the server to get a connection to. <code>null</code> for
* Local operations that do not require a server.
* @return A connection to the server, cached if available.
* @throws ConnectionException It was not possible to create a connection to the described server.
* @throws QueryException There is a transaction underway, but the new connection cannot turn off autocommit.
*/
Connection establishConnection(URI serverUri) throws ConnectionException, QueryException {
// get a new connection, or use the local one for non-server operations
Connection connection = null;
if (serverUri == null) {
connection = localStateConnection;
} else {
serverUri = ConnectionFactory.normalizeLocalUri(serverUri);
connection = transConnections.get(serverUri);
if (connection == null) {
connection = connectionFactory.newConnection(serverUri);
// update the connection if it needs to enter a current transaction
configureForTransaction(serverUri, connection);
}
}
return connection;
}
/**
* Set up the given connection for a current transaction, if one is active at the moment.
* @param connection The connection to configure. The dummy connection is not configured.
* @throws QueryException An error while setting up the connection for the transaction.
*/
private void configureForTransaction(URI serverUri, Connection connection) throws QueryException {
// If in a transaction, turn off autocommit - ignore the dummy connection
if (inTransaction && connection.getAutoCommit() && connection != localStateConnection) {
assert !(connection instanceof DummyConnection);
connection.setAutoCommit(false);
assert !transConnections.containsValue(connection);
transConnections.put(serverUri, connection);
}
}
/**
* Returns the current alias map. Needs to treat the internal interpreter
* explicitly as a TqlInterpreter.
* @deprecated Available to ItqlInterpreterBean only to support legacy requests.
* @return The mapping of namespaces to the URI for that space.
*/
Map<String,URI> getAliasesInUse() {
return ((TqlInterpreter)interpreter).getAliasMap();
}
/**
* Sets the current alias map. Needs to treat the internal interpreter
* explicitly as a TqlInterpreter.
* @deprecated Available to ItqlInterpreterBean only to support legacy requests.
*/
void setAliasesInUse(Map<String,URI> map) {
((TqlInterpreter)interpreter).setAliasMap(map);
}
/**
* Clears the last exception.
* @deprecated Available to ItqlInterpreterBean only to support legacy requests.
*/
void clearLastException() {
lastException = null;
}
/**
* Returns the internal local connection. Supports local operations for the current package.
* @return The local "state" connection.
*/
Connection getLocalConnection() {
return localStateConnection;
}
/**
* Commits all connections that started on a transaction. This operates directly
* on all known transacted connections.
* @throws QueryException One of the connections could not be successfully committed.
*/
void commitAll() throws QueryException {
handleTxOp(new Commit());
}
/**
* Rolls back all connections that started on a transaction. This oeprates directly
* on all known transacted connections.
* @throws QueryException One of the connections could not be successfully rolled back.
*/
void rollbackAll() throws QueryException {
handleTxOp(new Rollback());
}
/**
* Seeds the cache with a connection wrapping the given session.
* @deprecated Only for use by {@link ItqlInterpreterBean}.
* @param session The session to seed into the connection cache.
*/
void preSeedSession(Session session) {
// get back a new connection, and then drop it, since it will now be cached
try {
connectionFactory.newConnection(session);
} catch (ConnectionException e) {
logger.warn("Unable to use the given session for establishing a connection", e);
}
}
/**
* Perform any actions required on the update of the state of a connection.
* Most commands will skip through this method. Only transaction commands do anything.
* @param conn The connection whose state needs checking.
* @throws QueryException Can be caused by a failed change into a transaction.
*/
void updateConnectionsForTx(Connection conn, Command cmd) throws QueryException {
// check if the transaction state changed on a setAutocommit operations, or if the command is a Tx operation
if (inTransaction == conn.getAutoCommit() || cmd.isTxCommitRollback()) {
// check that transaction changes came from setAutoCommit commands
assert inTransaction != conn.getAutoCommit() ||
cmd instanceof org.mulgara.query.operation.SetAutoCommit ||
cmd instanceof org.mulgara.query.operation.Commit ||
cmd instanceof org.mulgara.query.operation.Rollback :
"Got a state change on " + cmd.getClass() + " instead of SetAutoCommit/Commit/Rollback";
// check that if we are starting a transaction then the transConnections list is empty
assert inTransaction != conn.getAutoCommit() || conn.getAutoCommit() || transConnections.isEmpty();
// save the number of active connections
int activeConnections = transConnections.size();
// handle the transaction operation
handleTxOp(cmd);
// check that if we have left a transaction, then the connection list is empty
assert inTransaction || transConnections.isEmpty();
// check that if we are still in a transaction, then the connection list has not changed
assert !inTransaction || activeConnections == transConnections.size();
}
}
/**
* This method wraps the simple loop of applying a command to all transaction connections.
* The wrapping is done to attempt the operation on all connections, despite exceptions
* being thrown.
* @param op The operation to end the transaction.
* @throws QueryException The operation could not be successfully performed.
*/
private void handleTxOp(Command op) throws QueryException {
// used to record the first exception, if there is one.
QueryException qe = null;
String errorMessage = null;
// Operate on all outstanding transactions.
Iterator<Connection> c = transConnections.values().iterator();
while (c.hasNext()) {
try {
// do the work
op.execute(c.next());
} catch (QueryException e) {
// store the details of the first exception only
if (qe != null) logger.error("Discarding subsequent exception during operation: " + op.getClass().getSimpleName(), e);
else {
qe = e;
errorMessage = op.getResultMessage();
}
} catch (Exception e) {
throw new QueryException("Unexpected exception during operation: " + op, e);
}
}
// will only get here once all connections were processed.
if (op instanceof TxOp) {
inTransaction = ((TxOp)op).stayInTx();
if (!inTransaction) transConnections.clear();
}
// if an exception was recorded, then throw it
if (qe != null) {
// remember the error message associated with the exception
op.setResultMessage(errorMessage);
throw qe;
}
}
}