/* $Id: Database.java 988245 2010-08-23 18:39:35Z kwright $ */ /** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.manifoldcf.core.database; import org.apache.manifoldcf.core.interfaces.*; import org.apache.manifoldcf.core.jdbcpool.*; import org.apache.manifoldcf.core.system.Logging; import org.apache.manifoldcf.core.system.ManifoldCF; import java.util.*; import java.sql.*; import javax.naming.*; import javax.sql.*; /** This class implements jskw.interfaces.IDatabase, and provides basic cached database services. * The actual cache keys are determined by layers above this. * It is expected that there is ONE of these objects per thread per database! If there are more, then * the transaction management will get screwed up (i.e. nobody will know what happened to the connection * handles...) */ public abstract class Database { public static final String _rcsid = "@(#)$Id: Database.java 988245 2010-08-23 18:39:35Z kwright $"; protected final ICacheManager cacheManager; protected final IThreadContext context; protected final String jdbcUrl; protected final String jdbcDriverClass; protected final String databaseName; protected String userName; protected String password; protected TransactionHandle th = null; protected WrappedConnection connection = null; protected boolean doRollback = false; protected boolean commitDone = false; protected int delayedTransactionDepth = 0; protected Map<String,Modifications> modificationsSet = new HashMap<String,Modifications>(); protected final long maxQueryTime; protected final boolean debug; protected final int maxDBConnections; protected static Random random = new Random(); protected final static String _TRANSACTION_ = "_TRANSACTION_"; public Database(IThreadContext context, String jdbcUrl, String jdbcDriverClass, String databaseName, String userName, String password) throws ManifoldCFException { this.context = context; this.jdbcUrl = jdbcUrl; this.jdbcDriverClass = jdbcDriverClass; this.databaseName = databaseName; this.userName = userName; this.password = password; this.maxQueryTime = ((long)LockManagerFactory.getIntProperty(context, ManifoldCF.databaseQueryMaxTimeProperty,60)) * 1000L; this.debug = LockManagerFactory.getBooleanProperty(context, ManifoldCF.databaseConnectionTrackingProperty, false); this.maxDBConnections = LockManagerFactory.getIntProperty(context, ManifoldCF.databaseHandleMaxcountProperty, 50); this.cacheManager = CacheManagerFactory.make(context); } /** Get the database name. This is often used as a cache key qualifier. *@return the database name. */ public String getDatabaseName() { return databaseName; } /** Get the current transaction id. *@return the current transaction identifier, or null if no transaction. */ public String getTransactionID() { if (th == null) return null; return th.getTransactionID(); } /** Abstract method to start a transaction */ protected void startATransaction() throws ManifoldCFException { } /** Abstract method to commit a transaction */ protected void commitCurrentTransaction() throws ManifoldCFException { } /** Abstract method to roll back a transaction */ protected void rollbackCurrentTransaction() throws ManifoldCFException { } /** Abstract method for explaining a query */ protected void explainQuery(String query, List params) throws ManifoldCFException { } /** Abstract method for mapping a column lookup name from resultset */ protected String mapLookupName(String rawColumnName, String rawLabelName) { return rawColumnName; } /** Abstract method for mapping a label name from resultset */ protected String mapLabelName(String rawLabelName) { return rawLabelName; } /** Prepare database for database creation step. * In order to do this, all connections to the back end must be closed. Since we have a pool, and a local * connection, these all need to be cleaned up. */ public void prepareForDatabaseCreate() throws ManifoldCFException { if (connection != null) { throw new ManifoldCFException("Can't do a database create within a transaction"); } ConnectionFactory.flush(); } /** Execute arbitrary database query, and optionally cache the result. Cached results are * returned for this operation if they are valid and appropriate. Note that any cached results * returned were only guaranteed to be pertinent at the time the cached result was obtained; the * actual data may become invalid due to other threads writing to the database. * This is NOT true, however, if a transaction is started. If a transaction was started for this * database within this thread context, then the query * will be executed within the transaction, and since the transaction is owned by the current * thread, no others will be able to disrupt its processing. * @param query is the actual query string. * @param params if not null, are prepared statement parameters. * @param cacheKeys is the set of cache keys that the query result will be cached against. If the * value for this parameter is null, then the query will not be cached. * @param invalidateKeys is the set of cache keys that the query will invalidate when the query occurs. * If this is null, then no keys will be invalidated. Note that if this is in a transaction, the * cache invalidation will only occur for queries that are part of the transaction, at least until * the transaction is committed. * @param queryClass describes the class of the query, for the purposes of LRU and expiration time. * The queryClass groups queries together, so that they are managed with a common set of timeouts * and maximum sizes. If null, then no expiration or LRU behavior will take place. * @param needResult is true if the result is needed. * @param maxReturn is the maximum number of rows to return. Use -1 for infinite. * @param spec is the result specification object, or null for standard. * @param returnLimits is a description of how to limit return results (in addition to the maxReturn value). * Pass null if no limits are desired. * @return the resultset */ public IResultSet executeQuery(String query, List params, StringSet cacheKeys, StringSet invalidateKeys, String queryClass, boolean needResult, int maxReturn, ResultSpecification spec, ILimitChecker returnLimits) throws ManifoldCFException { if (commitDone) throw new ManifoldCFException("Commit already done"); // System.out.println("Query: "+query); if (Logging.db.isDebugEnabled()) { Logging.db.debug("Requested query: [" + query + "]"); } // Make sure we can't cache a query that invalidates stuff if (!needResult) cacheKeys = null; // We do NOT automatically qualify the cache and invalidation keys with the database name. // This is a job that the caller will need to do, where required. // Create object description QueryDescription[] queryDescriptions = new QueryDescription[1]; QueryCacheExecutor executor; // Note: The caching effects of transactions are now handled by the cache manager. // All we do is tell it what we are doing. This is encapsulated by the transaction ID passed // to the cache methods. queryDescriptions[0] = new QueryDescription(databaseName,query,params,queryClass,cacheKeys,maxReturn,spec,returnLimits); executor = new QueryCacheExecutor(this,needResult); cacheManager.findObjectsAndExecute(queryDescriptions,invalidateKeys,executor,getTransactionID()); return executor.getResult(); } /** Get the current transaction type. Returns "READCOMMITTED" * outside of a transaction. */ public int getCurrentTransactionType() { if (th == null) return IDBInterface.TRANSACTION_READCOMMITTED; return th.getTransactionType(); } /** Begin a database transaction. This method call MUST be paired with an endTransaction() call, * or database handles will be lost. If the transaction should be rolled back, then signalRollback() should * be called before the transaction is ended. * It is strongly recommended that the code that uses transactions be structured so that a try block * starts immediately after this method call. The body of the try block will contain all direct or indirect * calls to executeQuery(). After this should be a catch for every exception type, including Error, which should call the * signalRollback() method, and rethrow the exception. Then, after that a finally{} block which calls endTransaction(). *@param transactionType describes the type of the transaction. */ public void beginTransaction(int transactionType) throws ManifoldCFException { if (Logging.db.isDebugEnabled()) Logging.db.debug("Beginning transaction of type "+Integer.toString(transactionType)); // Currently the cache management does absolutely nothing different for transactions of different types. // In practice this is not currently a problem, although a more rigorous treatment would involve taking greater // care to mirror the different types. // The "begin transaction" command itself is fired off in this module. Anything additional will be fired off after that // at the database implementation layer, which will incidentally cause any delayed transactions to actually be starte.d String enclosingID = (th==null)?null:th.getTransactionID(); delayedTransactionDepth++; th = new TransactionHandle(context,th,transactionType); cacheManager.startTransaction(th.getTransactionID(),enclosingID); doRollback = false; commitDone = false; } /** Synchronize internal transactions. */ protected void synchronizeTransactions() throws ManifoldCFException { while (delayedTransactionDepth > 0) { // Try starting the transaction // If failure, make CERTAIN that the little number does not get decremented! internalTransactionBegin(); delayedTransactionDepth--; } } /** Perform actual transaction begin. */ protected void internalTransactionBegin() throws ManifoldCFException { // Get a semipermanent connection if (connection == null) { connection = ConnectionFactory.getConnection(jdbcUrl,jdbcDriverClass,databaseName,userName,password, maxDBConnections,debug); try { // Initialize the connection (for HSQLDB) initializeConnection(connection.getConnection()); // Start a transaction startATransaction(); } catch (ManifoldCFException e) { if (e.getErrorCode() == ManifoldCFException.INTERRUPTED) { connection = null; throw e; } ConnectionFactory.releaseConnection(connection); connection = null; throw e; } catch (Error e) { ConnectionFactory.releaseConnection(connection); connection = null; throw e; } } else { try { startATransaction(); } catch (ManifoldCFException e) { if (e.getErrorCode() == ManifoldCFException.INTERRUPTED) { // Don't do anything else other than drop the connection on the floor connection = null; } throw e; } } } /** Perform the transaction commit. * Calling this method does not relieve the coder of the responsibility of calling endTransaction(), * as listed below. The purpose of a separate commit operation is to allow handling of situations where the * commit generates a TRANSACTION_ABORT signal. */ public void performCommit() throws ManifoldCFException { if (doRollback) return; if (delayedTransactionDepth == 0) { Logging.db.debug("Committing transaction!"); commitCurrentTransaction(); commitDone = true; } } /** Signal that a rollback should occur on the next endTransaction(). */ public void signalRollback() { doRollback = true; } /** End a database transaction, either performing a commit or a rollback (depending on whether * signalRollback() was called within the transaction). */ public void endTransaction() throws ManifoldCFException { Logging.db.debug("Ending transaction"); if (th == null) throw new ManifoldCFException("End transaction without begin!",ManifoldCFException.GENERAL_ERROR); TransactionHandle parentTransaction = th.getParent(); // If the database throws up on the commit or the rollback, above us there // will be no attempt to retry the transaction commit or rollback, so do NOT leave things // in an inconsistent state! As far as we are concerned, the transaction is over, end of // story. try { if (delayedTransactionDepth > 0) delayedTransactionDepth--; else { try { if (doRollback) { // Do a rollback in the database, and blow away cached queries (cached against the // database transaction key). if (!commitDone) { Logging.db.debug("Rolling transaction back!"); rollbackCurrentTransaction(); } else { doRollback = false; throw new ManifoldCFException("Cannot roll back an already committed transaction"); } } else { // Do a commit into the database, and blow away cached queries (cached against the // database transaction key). if (!commitDone) { Logging.db.debug("Committing transaction!"); commitCurrentTransaction(); } } } catch (ManifoldCFException e) { if (e.getErrorCode() == ManifoldCFException.INTERRUPTED) { // Drop the connection on the floor, so it cannot be reused. connection = null; } throw e; } finally { if (parentTransaction == null && connection != null) { ConnectionFactory.releaseConnection(connection); connection = null; } } } } finally { if (doRollback) { cacheManager.rollbackTransaction(th.getTransactionID()); } else { cacheManager.commitTransaction(th.getTransactionID()); } // Clear the signaling variables. This keeps them local to the transaction. commitDone = false; doRollback = false; th = parentTransaction; if (th == null) { if (doRollback) modificationsSet.clear(); else playbackModifications(); } } } /** Playback modifications */ private void playbackModifications() throws ManifoldCFException { Iterator<String> modIterator = modificationsSet.keySet().iterator(); while (modIterator.hasNext()) { String tableName = modIterator.next(); Modifications c = modificationsSet.get(tableName); noteModificationsNoTransactions(tableName,c.getInsertCount(),c.getModifyCount(),c.getDeleteCount()); } modificationsSet.clear(); } /** Note a number of inserts, modifications, or deletions to a specific table. This is so we can decide when to do appropriate maintenance. *@param tableName is the name of the table being modified. *@param insertCount is the number of inserts. *@param modifyCount is the number of updates. *@param deleteCount is the number of deletions. */ public void noteModifications(String tableName, int insertCount, int modifyCount, int deleteCount) throws ManifoldCFException { if (th != null) { // In a transaction; record for later Modifications c = modificationsSet.get(tableName); if (c == null) { c = new Modifications(); modificationsSet.put(tableName,c); } c.update(insertCount,modifyCount,deleteCount); } else noteModificationsNoTransactions(tableName,insertCount,modifyCount,deleteCount); } /** Protected method for receiving information about inserts, modifications, or deletions OUTSIDE of all transactions. */ protected void noteModificationsNoTransactions(String tableName, int insertCount, int modifyCount, int deleteCount) throws ManifoldCFException { } /** Sleep a random amount of time after a transaction abort. */ public long getSleepAmt() { // Amount should be between .5 and 1 minute, approx, to give things time to unwind return (long)(random.nextDouble() * 60000.0 + 500.0); } /** Sleep, as part of recovery from deadlock. */ public void sleepFor(long amt) throws ManifoldCFException { if (amt == 0L) return; try { ManifoldCF.sleep(amt); } catch (InterruptedException e) { throw new ManifoldCFException("Interrupted",e,ManifoldCFException.INTERRUPTED); } } /** Construct index hint clause. * On most databases this returns an empty string, but on MySQL this returns * a USE INDEX hint. It requires the name of an index. *@param tableName is the table the index is from. *@param description is the description of an index, which is expected to exist. *@return the query chunk that should go between the table names and the WHERE * clause. */ public String constructIndexHintClause(String tableName, IndexDescription description) throws ManifoldCFException { return ""; } /** Construct ORDER-BY clause meant for reading from an index. * Supply the field names belonging to the index, in order. * Also supply a corresponding boolean array, where TRUE means "ASC", and FALSE * means "DESC". *@param fieldNames are the names of the fields in the index that is to be used. *@param direction is a boolean describing the sorting order of the first term. *@return a query chunk, including "ORDER BY" text, which is appropriate for * at least ordering by the FIRST column supplied. */ public String constructIndexOrderByClause(String[] fieldNames, boolean direction) { if (fieldNames.length == 0) return ""; StringBuilder sb = new StringBuilder("ORDER BY "); sb.append(fieldNames[0]); if (direction) sb.append(" ASC"); else sb.append(" DESC"); return sb.toString(); } /** Construct an offset/limit clause. * This method constructs an offset/limit clause in the proper manner for the database in question. *@param offset is the starting offset number. *@param limit is the limit of result rows to return. *@return the proper clause, with no padding spaces on either side. */ public String constructOffsetLimitClause(int offset, int limit) { return constructOffsetLimitClause(offset,limit,false); } /** Construct an offset/limit clause. * This method constructs an offset/limit clause in the proper manner for the database in question. *@param offset is the starting offset number. *@param limit is the limit of result rows to return. *@param afterOrderBy is true if this offset/limit comes after an ORDER BY. *@return the proper clause, with no padding spaces on either side. */ public abstract String constructOffsetLimitClause(int offset, int limit, boolean afterOrderBy); /* Calculate the number of values a particular clause can have, given the values for all the other clauses. * For example, if in the expression x AND y AND z, x has 2 values and z has 1, find out how many values x can legally have * when using the buildConjunctionClause() method below. */ public int findConjunctionClauseMax(ClauseDescription[] otherClauseDescriptions) { // Base implementation uses "IN" for multiple values, since this seems to be widely accepted. return getMaxInClause(); } /** Obtain the maximum number of individual items that should be * present in an IN clause. Exceeding this amount will potentially cause the query performance * to drop. *@return the maximum number of IN clause members. */ public abstract int getMaxInClause(); /* Construct a conjunction clause, e.g. x AND y AND z, where there is expected to be an index (x,y,z,...), and where x, y, or z * can have multiple distinct values, The proper implementation of this method differs from database to database, because some databases * only permit index operations when there are OR's between clauses, such as x1 AND y1 AND z1 OR x2 AND y2 AND z2 ..., where others * only recognize index operations when there are lists specified for each, such as x IN (x1,x2) AND y IN (y1,y2) AND z IN (z1,z2). */ public String buildConjunctionClause(List outputParameters, ClauseDescription[] clauseDescriptions) { // Base implementation uses "IN" for multiple values, since this seems to be widely accepted. StringBuilder sb = new StringBuilder(); for (int i = 0 ; i < clauseDescriptions.length ; i++) { ClauseDescription cd = clauseDescriptions[i]; if (i > 0) sb.append(" AND "); sb.append(cd.getColumnName()); String operation = cd.getOperation(); List values = cd.getValues(); String joinColumn = cd.getJoinColumnName(); if (values != null) { if (values.size() > 1) { sb.append(" IN ("); for (int j = 0 ; j < values.size() ; j++) { if (j > 0) sb.append(","); sb.append("?"); outputParameters.add(values.get(j)); } sb.append(")"); } else { sb.append(operation).append("?"); outputParameters.add(values.get(0)); } } else if (joinColumn != null) { sb.append(operation).append(joinColumn); } else sb.append(operation); } return sb.toString(); } /** Class to keep track of modifications while we're in a transaction. */ protected static class Modifications { protected int insertCount = 0; protected int modifyCount = 0; protected int deleteCount = 0; public Modifications() { } public void update(int insertCount, int modifyCount, int deleteCount) { this.insertCount += insertCount; this.modifyCount += modifyCount; this.deleteCount += deleteCount; } public int getInsertCount() { return insertCount; } public int getModifyCount() { return modifyCount; } public int getDeleteCount() { return deleteCount; } } /** Thread used to execute queries. An instance of this thread is spun up every time a query is executed. This is necessary because JDBC does not * guarantee interruptability, and the Postgresql JDBC driver unfortunately eats all thread interrupts. So, we fire up a thread to do each interaction with * the database server, thus insuring that the owning thread remains interruptable and will therefore not block shutdown. */ protected class ExecuteQueryThread extends Thread { protected Connection connection; protected String query; protected List params; protected boolean bResults; protected int maxResults; protected ResultSpecification spec; protected ILimitChecker returnLimit; protected Throwable exception = null; protected IResultSet rval = null; public ExecuteQueryThread(Connection connection, String query, List params, boolean bResults, int maxResults, ResultSpecification spec, ILimitChecker returnLimit) { super(); setDaemon(true); this.connection = connection; this.query = query; this.params = params; this.bResults = bResults; this.maxResults = maxResults; this.spec = spec; this.returnLimit = returnLimit; } public void run() { try { // execute using the passed connection handle rval = execute(connection,query,params,bResults,maxResults,spec,returnLimit); } catch (Throwable e) { this.exception = e; } } public IResultSet finishUp() throws ManifoldCFException, InterruptedException { join(); Throwable thr = exception; if (thr != null) { if (thr instanceof ManifoldCFException) { // Nest the exceptions so there is a hope we actually see the context, while preserving the kind of error it is ManifoldCFException me = (ManifoldCFException)thr; throw new ManifoldCFException("Database exception: "+me.getMessage(),me.getCause(),me.getErrorCode()); } else if (thr instanceof Error) throw (Error)thr; else if (thr instanceof RuntimeException) throw (RuntimeException)thr; else throw new RuntimeException("Unknown exception: "+thr.getClass().getName()+": "+thr.getMessage(),thr); } return rval; } } /** Do query execution via a subthread, so the primary thread can be interrupted */ protected IResultSet executeViaThread(Connection connection, String query, List params, boolean bResults, int maxResults, ResultSpecification spec, ILimitChecker returnLimit) throws ManifoldCFException { if (connection == null) // This probably means that the thread was interrupted and the connection was abandoned. Just return null. return null; ExecuteQueryThread t = new ExecuteQueryThread(connection,query,params,bResults,maxResults,spec,returnLimit); try { t.start(); return t.finishUp(); } catch (InterruptedException e) { // Try to kill the background thread - but we can't wait for it... t.interrupt(); interruptCleanup(connection); // We need the caller to abandon any connections left around, so rethrow in a way that forces them to process the event properly. throw new ManifoldCFException(e.getMessage(),e,ManifoldCFException.INTERRUPTED); } } /** This method must clean up after a execute query thread has been forcibly interrupted. * It has been separated because some JDBC drivers don't handle forcible interrupts * appropriately. */ protected void interruptCleanup(Connection connection) { // VERY IMPORTANT: Try to close the connection, so nothing is left dangling. The connection will be abandoned anyhow. try { if (!connection.getAutoCommit()) connection.rollback(); connection.close(); } catch (Exception e2) { } } /** This method does NOT appear in any interface; it is here to * service the cache object. */ protected IResultSet executeUncachedQuery(String query, List params, boolean bResults, int maxResults, ResultSpecification spec, ILimitChecker returnLimit) throws ManifoldCFException { if (connection != null) { try { return executeViaThread(connection.getConnection(),query,params,bResults,maxResults,spec,returnLimit); } catch (ManifoldCFException e) { if (e.getErrorCode() == ManifoldCFException.INTERRUPTED) // drop the connection object on the floor, so it cannot possibly be reused connection = null; throw e; } } else { // Grab a connection WrappedConnection tempConnection = ConnectionFactory.getConnection(jdbcUrl,jdbcDriverClass,databaseName,userName,password, maxDBConnections,debug); try { // Initialize the connection (for HSQLDB) initializeConnection(tempConnection.getConnection()); return executeViaThread(tempConnection.getConnection(),query,params,bResults,maxResults,spec,returnLimit); } catch (ManifoldCFException e) { if (e.getErrorCode() == ManifoldCFException.INTERRUPTED) // drop the connection object on the floor, so it cannot possibly be reused tempConnection = null; throw e; } finally { if (tempConnection != null) ConnectionFactory.releaseConnection(tempConnection); } } } // These are protected helper methods /** Initialize the connection (for HSQLDB). * HSQLDB has a great deal of session state, and no way to pool individual connections based on it. * So, every time we pull a connection off the pool we have to execute a number of statements on it * before it can work reliably for us. This is the abstraction that permits that to happen. *@param connection is the JDBC connection. */ protected void initializeConnection(Connection connection) throws ManifoldCFException { // Default implementation does nothing; override to make special stuff happen. } /** Run a query. No caching is involved at all at this level. * @param query String the query string * @param bResults boolean whether to load the resultset or not * @param maxResults is the maximum number of results to load: -1 if all * @param params List if params !=null, use preparedStatement */ protected IResultSet execute(Connection connection, String query, List params, boolean bResults, int maxResults, ResultSpecification spec, ILimitChecker returnLimit) throws ManifoldCFException { IResultSet rval = null; try { try { ResultSet rs; long queryStartTime = 0L; if (Logging.db.isDebugEnabled()) { queryStartTime = System.currentTimeMillis(); Logging.db.debug("Actual query: [" + query + "]"); if (params != null) { int i = 0; while (i < params.size()) { Logging.db.debug(" Parameter " + i + ": '" + params.get(i).toString() + "'"); i++; } } } if (params==null) { //stmt = _connection.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, // ResultSet.CONCUR_READ_ONLY); // lightest statement type Statement stmt = connection.createStatement(); try { stmt.execute(query); rs = stmt.getResultSet(); try { // Suck data from resultset rval = getData(rs,bResults,maxResults,spec,returnLimit); } finally { if (rs != null) rs.close(); } } finally { stmt.close(); } } else { PreparedStatement ps = connection.prepareStatement(query); try { loadPS(ps, params); if (bResults) { rs = ps.executeQuery(); try { // Suck data from resultset rval = getData(rs,true,maxResults,spec,returnLimit); } finally { if (rs != null) rs.close(); } } else { ps.executeUpdate(); rval = getData(null,false,0,spec,null); } } finally { ps.close(); } } if (Logging.db.isDebugEnabled()) Logging.db.debug("Done actual query ("+new Long(System.currentTimeMillis()-queryStartTime).toString()+"ms): ["+query+"]"); } catch (java.sql.SQLException e) { // There are a lot of different sorts of error that can be embedded here. Unfortunately, it's database dependent how // to interpret the error. So toss a generic error, and let the caller figure out if it needs to treat it differently. throw new ManifoldCFException("SQLException doing query"+((e.getSQLState() != null)?" ("+e.getSQLState()+")":"")+": "+e.getMessage(),e,ManifoldCFException.DATABASE_CONNECTION_ERROR); } } finally { // Cleanup of input parameters ALWAYS occurs, because whether we succeed or fail, we are done with any input streams. if (params != null) cleanupParameters(params); } return rval; } // Read data from a resultset protected IResultSet getData(ResultSet rs, boolean bResults, int maxResults, ResultSpecification spec, ILimitChecker returnLimit) throws ManifoldCFException { RSet results = new RSet(); // might be empty but not an error try { try { if (rs != null) { int colcount = 0; String[] resultCols = null; String[] resultLabels = null; // Optionally we're going to suck the data // out of the db and return it in a // readonly structure ResultSetMetaData rsmd = rs.getMetaData(); if (rsmd != null) { colcount = rsmd.getColumnCount(); //LogBean.db.debug(colcount + " columns returned."); resultCols = new String[colcount]; resultLabels = new String[colcount]; for (int i = 0; i < colcount; i++) { String labelName = rsmd.getColumnLabel(i+1); resultCols[i] = mapLookupName(rsmd.getColumnName(i+1),labelName); resultLabels[i] = mapLabelName(labelName); } } if (bResults) { if (colcount == 0) { // This is an error situation; if a result with no columns is // necessary, bResults must be false!!! throw new ManifoldCFException("Empty query, no columns returned",ManifoldCFException.GENERAL_ERROR); } while (rs.next() && (maxResults == -1 || maxResults > 0) && (returnLimit == null || returnLimit.checkContinue())) { Object value; RRow m = new RRow(); // We have 'colcount' cols to look thru for (int i = 0; i < colcount; i++) { String key = resultCols[i]; // System.out.println("Key = "+key); int colnum = findColumn(rs,key); value = null; if (colnum > -1) { value = getObject(rs,rsmd,colnum,(spec == null)?ResultSpecification.FORM_DEFAULT:spec.getForm(key.toLowerCase(Locale.ROOT))); } //System.out.println(" Key = '"+resultLabels[i]+"', value = "+((value==null)?"NULL":value.toString())); m.put(resultLabels[i], value); } // See if we should include this row boolean include = true; if (returnLimit != null) { include = returnLimit.checkInclude(m); } if (include) { if (maxResults != -1) maxResults--; results.addRow(m); } else { // As a courtesy, clean up any BinaryInput objects in the row we are skipping Iterator iter = m.getColumns(); while (iter.hasNext()) { String columnName = (String)iter.next(); Object colValue = m.getValue(columnName); if (colValue instanceof PersistentDatabaseObject) ((PersistentDatabaseObject)colValue).discard(); } } } } } } catch (java.sql.SQLException e) { throw new ManifoldCFException("SQLException getting resultset"+((e.getSQLState() != null)?" ("+e.getSQLState()+")":"")+": "+e.getMessage(),e,ManifoldCFException.DATABASE_CONNECTION_ERROR); } } catch (Throwable e) { // Clean up resultset before continuing int i = 0; while (i < results.getRowCount()) { IResultRow row = results.getRow(i++); int j = 0; Iterator iter = row.getColumns(); while (iter.hasNext()) { String colName = (String)iter.next(); Object o = row.getValue(colName); if (o instanceof PersistentDatabaseObject) ((PersistentDatabaseObject)o).discard(); } } if (e instanceof ManifoldCFException) throw (ManifoldCFException)e; if (e instanceof RuntimeException) throw (RuntimeException)e; if (e instanceof Error) throw (Error)e; throw new Error("Unexpected exception caught: "+e.getMessage(),e); } return results; } // pass params to preparedStatement protected static void loadPS(PreparedStatement ps, List data) throws java.sql.SQLException, ManifoldCFException { if (data!=null) { for (int i = 0; i < data.size(); i++) { // If the input type is a string, then set it as such. // Otherwise, if it's an input stream, we make a blob out of it. Object x = data.get(i); if (x instanceof String) { String value = (String)x; // letting database do lame conversion! ps.setString(i+1, value); } if (x instanceof BinaryInput) { BinaryInput value = (BinaryInput)x; long length = value.getLength(); // System.out.println("Blob length on write = "+Long.toString(value.getLength())); ps.setBinaryStream(i+1,value.getStream(),(length == -1L)?Integer.MAX_VALUE:(int)length); } if (x instanceof CharacterInput) { CharacterInput value = (CharacterInput)x; long length = value.getCharacterLength(); ps.setCharacterStream(i+1,value.getStream(),(length == -1L)?Integer.MAX_VALUE:(int)length); } if (x instanceof java.util.Date) { ps.setDate(i+1,new java.sql.Date(((java.util.Date)x).getTime())); } if (x instanceof Long) { ps.setLong(i+1,((Long)x).longValue()); } if (x instanceof TimeMarker) { ps.setTimestamp(i+1,new java.sql.Timestamp(((TimeMarker)x).longValue())); } if (x instanceof Double) { ps.setDouble(i+1,((Double)x).doubleValue()); } if (x instanceof Integer) { ps.setInt(i+1,((Integer)x).intValue()); } if (x instanceof Float) { ps.setFloat(i+1,((Float)x).floatValue()); } } } } /** Clean up parameters after query has been triggered. */ protected static void cleanupParameters(List data) throws ManifoldCFException { if (data != null) { for (Object x : data) { if (x instanceof PersistentDatabaseObject) { ((PersistentDatabaseObject)x).doneWithStream(); } } } } protected int findColumn(ResultSet rs, String name) throws ManifoldCFException { try { return rs.findColumn(name); } catch (SQLException e) { return -1; } catch (Exception e) { throw new ManifoldCFException("Error finding " + name + " in resultset: "+e.getMessage(),e,ManifoldCFException.DATABASE_ERROR); } } protected Blob getBLOB(ResultSet rs, int col) throws ManifoldCFException { try { return rs.getBlob(col); } catch (java.sql.SQLException e) { throw new ManifoldCFException("SQLException in getBlob"+((e.getSQLState() != null)?" ("+e.getSQLState()+")":"")+": "+e.getMessage(),e,ManifoldCFException.DATABASE_CONNECTION_ERROR); } catch (Exception sqle) { throw new ManifoldCFException("Error in getBlob",sqle,ManifoldCFException.DATABASE_ERROR); } } protected boolean isBLOB(ResultSetMetaData rsmd, int col) throws ManifoldCFException { try { int type = rsmd.getColumnType(col); return (type == java.sql.Types.BLOB); } catch (java.sql.SQLException e) { throw new ManifoldCFException("SQLException doing isBlob("+col+")"+((e.getSQLState() != null)?" ("+e.getSQLState()+")":"")+": "+e.getMessage(),e,ManifoldCFException.DATABASE_CONNECTION_ERROR); } catch (Exception sqle) { throw new ManifoldCFException("Error in isBlob("+col+"): "+sqle.getMessage(),sqle,ManifoldCFException.DATABASE_ERROR); } } protected boolean isBinary(ResultSetMetaData rsmd, int col) throws ManifoldCFException { try { int type = rsmd.getColumnType(col); return (type == java.sql.Types.VARBINARY || type == java.sql.Types.BINARY || type == java.sql.Types.LONGVARBINARY); } catch (java.sql.SQLException e) { throw new ManifoldCFException("SQLException doing isBinary("+col+")"+((e.getSQLState() != null)?" ("+e.getSQLState()+")":"")+": "+e.getMessage(),e,ManifoldCFException.DATABASE_CONNECTION_ERROR); } catch (Exception sqle) { throw new ManifoldCFException("Error in isBinary("+col+"): "+sqle.getMessage(),sqle,ManifoldCFException.DATABASE_ERROR); } } protected Object getObject(ResultSet rs, ResultSetMetaData rsmd, int col, int desiredForm) throws ManifoldCFException { Object result = null; try { try { //System.out.println(" Column "+rsmd.getColumnLabel(col)+" is of type "+rsmd.getColumnType(col)); if (isBLOB(rsmd,col)) { // System.out.println("It's a blob!"); Blob blob = getBLOB(rs,col); if (blob != null) { // Create a tempfileinput object! // Cleanup should happen by the user of the resultset. // System.out.println(" Blob length = "+Long.toString(blob.length())); result = new TempFileInput(blob.getBinaryStream(),blob.length()); } } else if (isBinary(rsmd,col)) { java.io.InputStream is = rs.getBinaryStream(col); if (is != null) { // Create a tempfileinput object! // Cleanup should happen by the user of the resultset. result = new TempFileInput(is); } } else { Timestamp timestamp; java.sql.Date date; Clob clob; String resultString; int colType = rsmd.getColumnType(col); switch (colType) { case java.sql.Types.CHAR : case java.sql.Types.VARCHAR : switch (desiredForm) { case ResultSpecification.FORM_DEFAULT: case ResultSpecification.FORM_STRING: if ((resultString = rs.getString(col)) != null) { // We used to truncate result based on columnDisplaySize, but that (a) didn't seem to be // helping on modern JDBC drivers, and (b) was completely busted on MySQL, so we no longer do it. /* if (rsmd.getColumnDisplaySize(col) < resultString.length()) { result = resultString.substring(0,rsmd.getColumnDisplaySize(col)); } else result = resultString; */ result = resultString; } break; case ResultSpecification.FORM_STREAM: result = new TempFileCharacterInput(rs.getCharacterStream(col)); break; default: throw new ManifoldCFException("Illegal form requested for column "+Integer.toString(col)+": "+Integer.toString(desiredForm)); } break; case java.sql.Types.CLOB : switch (desiredForm) { case ResultSpecification.FORM_DEFAULT: case ResultSpecification.FORM_STRING: if ((clob = rs.getClob(col)) != null) { result = clob.getSubString(1, (int) clob.length()); } break; case ResultSpecification.FORM_STREAM: result = new TempFileCharacterInput(rs.getCharacterStream(col)); break; default: throw new ManifoldCFException("Illegal form requested for column "+Integer.toString(col)+": "+Integer.toString(desiredForm)); } break; case java.sql.Types.BIGINT : long l = rs.getLong(col); if (!rs.wasNull()) result = new Long(l); break; case java.sql.Types.INTEGER : int i = rs.getInt(col); if (!rs.wasNull()) result = new Integer(i); break; case java.sql.Types.SMALLINT: short s = rs.getShort(col); if (!rs.wasNull()) result = new Short(s); break; case java.sql.Types.REAL : case java.sql.Types.FLOAT : float f = rs.getFloat(col); if (!rs.wasNull()) result = new Float(f); break; case java.sql.Types.DOUBLE : double d = rs.getDouble(col); if (!rs.wasNull()) result = new Double(d); break; case java.sql.Types.DATE : if ((date = rs.getDate(col)) != null) { result = new java.util.Date(date.getTime()); } break; case java.sql.Types.TIMESTAMP : if ((timestamp = rs.getTimestamp(col)) != null) { result = new TimeMarker(timestamp.getTime()); } break; case java.sql.Types.BOOLEAN : boolean b = rs.getBoolean(col); if (!rs.wasNull()) result = new Boolean(b); break; case java.sql.Types.BLOB: throw new ManifoldCFException("BLOB is not a string, column = " + col,ManifoldCFException.GENERAL_ERROR); default : switch (desiredForm) { case ResultSpecification.FORM_DEFAULT: case ResultSpecification.FORM_STRING: result = rs.getString(col); break; case ResultSpecification.FORM_STREAM: result = new TempFileCharacterInput(rs.getCharacterStream(col)); break; default: throw new ManifoldCFException("Illegal form requested for column "+Integer.toString(col)+": "+Integer.toString(desiredForm)); } break; } if (rs.wasNull()) { if (result instanceof CharacterInput) ((CharacterInput)result).discard(); result = null; } } } catch (java.sql.SQLException e) { throw new ManifoldCFException("SQLException doing getObject()"+((e.getSQLState() != null)?" ("+e.getSQLState()+")":"")+": "+e.getMessage(),e,ManifoldCFException.DATABASE_CONNECTION_ERROR); } } catch (Throwable e) { if (result instanceof PersistentDatabaseObject) ((PersistentDatabaseObject)result).discard(); if (e instanceof ManifoldCFException) throw (ManifoldCFException)e; if (e instanceof RuntimeException) throw (RuntimeException)e; if (e instanceof Error) throw (Error)e; throw new Error("Unexpected exception caught: "+e.getMessage(),e); } return result; } /** This object is meant to execute within a cache manager call. It contains all knowledge needed to * perform any query, including a parameterized one. It may (or may not) be also passed a transaction * handle, depending on whether or not a transaction is currently underway. * Nevertheless, all database access, save transaction setup and teardown, takes place inside this class. * Even uncached queries will be done here; the cache manager will simply not keep the result around * afterwards. */ public static class QueryCacheExecutor extends org.apache.manifoldcf.core.cachemanager.ExecutorBase { // We store only those things that will not come in from the object description. protected Database database; protected boolean needResult; protected IResultSet resultset = null; public QueryCacheExecutor(Database database, boolean needResult) { super(); this.database = database; this.needResult = needResult; } /** Fetch the result. No errors are possible at this time; they would have already * occurred... */ public IResultSet getResult() { return resultset; } /** Create a new object to operate on and cache. This method is called only * if the specified object is NOT available in the cache. The specified object * should be created and returned; if it is not created, it means that the * execution cannot proceed, and the execute() method will not be called. * @param objectDescriptions are the unique identifiers of the objects. * @return the newly created objects to cache, or null, if any object cannot be created. */ public Object[] create(ICacheDescription[] objectDescriptions) throws ManifoldCFException { // Perform the requested query, within the appropriate transaction object. // Call the database object to do this Object[] rval = new Object[objectDescriptions.length]; int i = 0; while (i < objectDescriptions.length) { database.synchronizeTransactions(); QueryDescription description = (QueryDescription)objectDescriptions[i]; ILimitChecker limit = description.getReturnLimit(); ResultSpecification spec = description.getResultSpecification(); // I've prevented us from ever caching things that have limit objects // at a higher level... // if (limit != null) // limit = limit.duplicate(); // ResultSpecification objects are considered "read only" once passed to the cache, so duplication is unneeded. // if (spec != null) // spec = spec.duplicate(); long startTime = System.currentTimeMillis(); rval[i] = database.executeUncachedQuery(description.getQuery(),description.getParameters(),needResult, description.getMaxReturn(),spec,limit); long endTime = System.currentTimeMillis(); if (endTime-startTime > database.maxQueryTime && description.getQuery().length() >= 6 && ("SELECT".equalsIgnoreCase(description.getQuery().substring(0,6)) || "UPDATE".equalsIgnoreCase(description.getQuery().substring(0,6)))) { Logging.db.warn("Found a long-running query ("+new Long(endTime-startTime).toString()+" ms): ["+description.getQuery()+"]"); if (description.getParameters() != null) { int j = 0; while (j < description.getParameters().size()) { Logging.db.warn(" Parameter " + j + ": '" + description.getParameters().get(j).toString() + "'"); j++; } } try { database.explainQuery(description.getQuery(),description.getParameters()); } catch (ManifoldCFException e) { // We need to know if explain generated a TRANSACTION_ABORT. If so we have to rethrow it. if (e.getErrorCode() == e.DATABASE_TRANSACTION_ABORT || e.getErrorCode() == e.INTERRUPTED) throw e; // Eat the exception Logging.db.warn("Explain failed with error "+e.getMessage(),e); } } i++; } return rval; } /** Notify the implementing class of the existence of a cached version of the * object. The object is passed to this method so that the execute() method below * will have it available to operate on. This method is also called for all objects * that are freshly created as well. * @param objectDescription is the unique identifier of the object. * @param cachedObject is the cached object. */ public void exists(ICacheDescription objectDescription, Object cachedObject) throws ManifoldCFException { // System.out.println("Object created or found: "+objectDescription.getCriticalSectionName()); // Save the resultset for return resultset = (IResultSet)cachedObject; } /** Perform the desired operation. This method is called after either createGetObject() * or exists() is called for every requested object. */ public void execute() throws ManifoldCFException { // Does nothing at all; the query would already have been done } } }