/* * Copyright (C) 2006-2008 Alfresco Software Limited. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * As a special exception to the terms and conditions of version 2.0 of * the GPL, you may redistribute this Program in connection with Free/Libre * and Open Source Software ("FLOSS") applications as described in Alfresco's * FLOSS exception. You should have recieved a copy of the text describing * the FLOSS exception, and it is also available here: * http://www.alfresco.com/legal/licensing" */ package org.alfresco.jlan.server.filesys.db; import java.io.File; import java.sql.Connection; import java.sql.SQLException; import org.alfresco.jlan.debug.Debug; import org.alfresco.jlan.server.config.InvalidConfigurationException; import org.alfresco.jlan.server.filesys.FileInfo; import org.alfresco.jlan.server.filesys.loader.DeleteFileRequest; import org.alfresco.jlan.server.filesys.loader.FileRequest; import org.alfresco.jlan.server.filesys.loader.FileRequestQueue; import org.alfresco.jlan.server.filesys.loader.MultipleFileRequest; import org.alfresco.jlan.server.filesys.loader.SingleFileRequest; import org.alfresco.jlan.util.MemorySize; import org.alfresco.jlan.util.db.DBConnectionPool; import org.alfresco.jlan.util.db.DBConnectionPoolListener; import org.springframework.extensions.config.ConfigElement; /** * JDBC Database Interface Abstract Class * * <p>Provides the standard variables for a JDBC based database interface, including the parsing of the * parameters in the initialization method. * * @author gkspencer */ public abstract class JdbcDBInterface implements DBInterface, DBConnectionPoolListener { // Constants // // Default database table names public final static String FileSysTable = "JLANFileSys"; public final static String StreamsTable = "JLANStreams"; public final static String RetentionTable = "JLANRetain"; public final static String QueueTable = "JLANQueue"; public final static String TransactQueueTable = "JLANTransQueue"; public final static String DataTable = "JLANData"; public final static String JarDataTable = "JLANJarData"; public final static String ObjectIdTable = "JLANObjectIds"; public final static String SymLinkTable = "JLANSymLinks"; // Number of connections to allocate in the connection pool public final static int NumPoolConnections = 5; // Special characters that must be escaped in file/directory names protected String m_specialChars = "\'\"\\"; // Default file data fragment size public final static long DefaultFragSize = MemorySize.MEGABYTE / 2; // 1/2Mb public final static long MinFragSize = MemorySize.KILOBYTE * 64L; // 64Kb public final static long MaxFragSize = MemorySize.GIGABYTE; // 1Gb // Database device context protected DBDeviceContext m_dbCtx; // Supported/requested database features private int m_features; private int m_reqFeatures; // JDBC driver class protected String m_driver; // JDBC connection string protected String m_dsn; // Username and password protected String m_userName; protected String m_password; // Database table that contains the file system structure records, streams information // records, data retention information records, file loader queue and file loader transaction // queue and file data tables. protected String m_structTable; protected String m_streamTable; protected String m_retentionTable; protected String m_queueTable; protected String m_transactTable; protected String m_dataTable; protected String m_jarDataTable; protected String m_objectIdTable; protected String m_symLinkTable; // Retention period, milliseconds to add to current date/time value, or -1 if not enabled protected long m_retentionPeriod = -1; // Database connection pool protected DBConnectionPool m_connPool; protected int m_dbInitConns = DBConnectionPool.DefaultMinSize; protected int m_dbMaxConns = DBConnectionPool.DefaultMaxSize; protected int m_onlineCheckInterval; // Data fragment size to store per BLOB when the file data is stored in the database protected long m_dataFragSize = DefaultFragSize; // Pending file ssave requests, used when the database goes offline protected FileRequestQueue m_pendingSaveRequests; // Debug and SQL debug enable flags protected boolean m_debug; protected boolean m_sqlDebug; // Lock file path private String m_lockFile; // Use crash recovery folder private boolean m_crashRecovery; /** * Default constructor */ public JdbcDBInterface() { m_features = getSupportedFeatures(); } /** * Determine if the database interface supports the specified feature * * @param feature int * @return boolean */ public final boolean supportsFeature(int feature) { return ( m_features & feature) != 0 ? true : false; } /** * Request the specified database features be enabled * * @param featureMask int * @exception DBException */ public void requestFeatures(int featureMask) throws DBException { // Get the database interface supported features int supFeatures = getSupportedFeatures(); // Check if there are any unsupported features requested if ((featureMask | supFeatures) != supFeatures) throw new DBException("Unsupported feature requested"); // Set the requested features m_reqFeatures = featureMask; } /** * Get the supported database features mask * * @return int */ protected abstract int getSupportedFeatures(); /** * Check if the crash recovery folder is enabled * * @return boolean */ public final boolean hasCrashRecovery() { return m_crashRecovery; } /** * Check if data retention is enabled * * @return boolean */ public final boolean isRetentionEnabled() { return (m_reqFeatures & FeatureRetention) != 0 ? true : false; } /** * Check if NTFS streams are enabled * * @return boolean */ public final boolean isNTFSEnabled() { return (m_reqFeatures & FeatureNTFS) != 0 ? true : false; } /** * Check if the database queue is enabled * * @return boolean */ public final boolean isQueueEnabled() { return (m_reqFeatures & FeatureQueue) != 0 ? true : false; } /** * Check if database file data load/save is enabled * * @return boolean */ public final boolean isDataEnabled() { return (m_reqFeatures & FeatureData) != 0 ? true : false; } /** * Check if database Jar file data load/save is enabled * * @return boolean */ public final boolean isJarDataEnabled() { return (m_reqFeatures & FeatureJarData) != 0 ? true : false; } /** * Check if the file id/object id mapping feature is enabled * * @return boolean */ public final boolean isObjectIdEnabled() { return (m_reqFeatures & FeatureObjectId) != 0 ? true : false; } /** * Check if the symbolic links feature is enabled * * @return boolean */ public final boolean isSymbolicLinksEnabled() { return ( m_reqFeatures & FeatureSymLinks) != 0 ? true : false; } /** * Get the lock file * * @return String */ protected final String getLockFile() { return m_lockFile; } /** * Set the lock file * * @param lockFile String */ protected final void setLockFile(String lockFile) { m_lockFile = lockFile; } /** * Initialize the database interface * * @param context DBDeviceContext * @param params NameValueList * @throws InvalidConfigurationException */ public void initializeDatabase(DBDeviceContext context, ConfigElement params) throws InvalidConfigurationException { // Save the context m_dbCtx = context; // Parse the standard JDBC database interface parameters ConfigElement nameVal = null; nameVal = params.getChild("DSN"); if ( nameVal != null) m_dsn = nameVal.getValue(); nameVal = params.getChild("Username"); if ( nameVal != null) m_userName = nameVal.getValue(); nameVal = params.getChild("Password"); if ( nameVal != null) m_password = nameVal.getValue(); nameVal = params.getChild("FileSystemTable"); if ( nameVal != null) m_structTable = nameVal.getValue(); else m_structTable = FileSysTable; nameVal = params.getChild("StreamsTable"); if ( nameVal != null) m_streamTable = nameVal.getValue(); else m_streamTable = StreamsTable; nameVal = params.getChild("RetentionTable"); if ( nameVal != null) m_retentionTable = nameVal.getValue(); else m_retentionTable = RetentionTable; nameVal = params.getChild("QueueTable"); if ( nameVal != null) m_queueTable = nameVal.getValue(); else m_queueTable = QueueTable; nameVal = params.getChild("TransactQueueTable"); if ( nameVal != null) m_transactTable = nameVal.getValue(); else m_transactTable = TransactQueueTable; nameVal = params.getChild("DataTable"); if ( nameVal != null) m_dataTable = nameVal.getValue(); else m_dataTable = DataTable; nameVal = params.getChild("JarDataTable"); if ( nameVal != null) m_jarDataTable = nameVal.getValue(); else m_jarDataTable = JarDataTable; nameVal = params.getChild("ObjectIdTable"); if ( nameVal != null) m_objectIdTable = nameVal.getValue(); else m_objectIdTable = ObjectIdTable; nameVal = params.getChild( "SymLinksTable"); if ( nameVal != null) m_symLinkTable = nameVal.getValue(); else m_symLinkTable = SymLinkTable; // Check if the database connection pool initial and maximum size has been specified nameVal = params.getChild("ConnectionPool"); if ( nameVal != null) { try { // Check for a single value or split initial/maximum values String numVal = nameVal.getValue(); int pos = numVal.indexOf(':'); if ( pos == -1) { // Use the same number of read and write worker threads m_dbMaxConns = Integer.parseInt(numVal); } else { // Split the string value into read and write values, and convert to integers String val = numVal.substring(0,pos); m_dbInitConns = Integer.parseInt(val); val = numVal.substring(pos + 1); m_dbMaxConns = Integer.parseInt(val); } // Range check the initial/maximum connection pool sizes if ( m_dbInitConns < DBConnectionPool.MinimumConnections || m_dbInitConns > m_dbMaxConns) throw new InvalidConfigurationException("Database interface invalid initial connections value"); if ( m_dbMaxConns > DBConnectionPool.MaximumConnections || m_dbMaxConns < m_dbInitConns) throw new InvalidConfigurationException("Database interface invalid maximum connections value"); } catch (NumberFormatException ex) { throw new InvalidConfigurationException("Database interface invalid ConnectionPool value, " + ex.toString()); } } // Check if the database online check interval has been specified nameVal = params.getChild("OnlineCheckInterval"); if ( nameVal != null) { try { // Parse the online check interval value m_onlineCheckInterval = Integer.parseInt( nameVal.getValue()); if ( m_onlineCheckInterval < 1 || m_onlineCheckInterval > 30) throw new InvalidConfigurationException( "Database online check interval out of valid range (1-30"); } catch ( NumberFormatException ex) { throw new InvalidConfigurationException("Database online check interval value invalid, " + nameVal.getValue()); } } // Check if debug output is enabled if ( params.getChild("Debug") != null) m_debug = true; // Check if SQL debug output is enabled if ( params.getChild("SQLDebug") != null) m_sqlDebug = true; // Copy the retention period from the context, value will be -1 if not enabled m_retentionPeriod = context.getRetentionPeriod(); // Check if the crash recovery folder is enabled if ( params.getChild("useCrashRecovery") != null) m_crashRecovery = true; } /** * Check if the database is online, return the database connection pool status * * @return boolean */ public boolean isOnline() { // Check the connection pool status if ( m_connPool != null) { // Check if the connection pool is online if ( m_connPool.isOnline()) return true; // Try and open a connection to the database to see if it's back online Connection conn = m_connPool.getConnection(); if ( conn != null) m_connPool.releaseConnection(conn); // Return the latest connection pool status return m_connPool.isOnline(); } // Connection pool is not valid return false; } /** * Shutdown the database interface * * @param context DBDeviceContext */ public void shutdownDatabase(DBDeviceContext context) { // Close the database connection pool if ( m_connPool != null) m_connPool.closePool(); } /** * Get the database connection, open a new connection if required * * @return Connection * @exception SQLException */ protected final Connection getConnection() throws SQLException { // Get a database connection Connection conn = m_connPool.getConnection(); if ( conn == null) throw new SQLException("Failed to get database connection"); // Return the database connection return conn; } /** * Get the database connection, open a new connection if required * * @param leaseTime long * @return Connection * @exception SQLException */ protected final Connection getConnection(long leaseTime) throws SQLException { // Get a database connection Connection conn = m_connPool.getConnection(leaseTime); if ( conn == null) throw new SQLException("Failed to get database connection"); // Return the database connection return conn; } /** * Release a database connection back to the connection pool * * @param conn Connection */ protected final void releaseConnection(Connection conn) { // Release the connection to the available pool m_connPool.releaseConnection(conn); } /** * Access the database connection pool * * @return DBConnectionPool */ protected final DBConnectionPool getConnectionPool() { return m_connPool; } /** * Get the driver class name * * @return String */ protected final String getDriverName() { return m_driver; } /** * Get the connection string * * @return String */ protected final String getDSNString() { return m_dsn; } /** * Return the database user name * * @return String */ protected final String getUserName() { return m_userName; } /** * Return the database password * * @return String */ protected final String getPassword() { return m_password; } /** * Return the file system structure table name * * @return String */ protected final String getFileSysTableName() { return m_structTable; } /** * Check if the NTFS streams table name is valid * * @return boolean */ protected final boolean hasStreamsTableName() { return m_streamTable != null ? true : false; } /** * Return the streams table name * * @return String */ protected final String getStreamsTableName() { return m_streamTable; } /** * Check if the retention table name is valid * * @return boolean */ protected final boolean hasRetentionTableName() { return m_retentionTable != null ? true : false; } /** * Return the retention table name * * @return String */ protected final String getRetentionTableName() { return m_retentionTable; } /** * Determine if the retention period is enabled * * @return boolean */ protected final boolean hasRetentionPeriod() { return m_retentionPeriod != -1L ? true : false; } /** * Return the retention period, in milliseconds * * @return long */ protected final long getRetentionPeriod() { return m_retentionPeriod; } /** * Check if the file loader data table name is valid * * @return boolean */ protected final boolean hasDataTableName() { return m_dataTable != null ? true : false; } /** * Return the file loader data table name * * @return String */ protected final String getDataTableName() { return m_dataTable; } /** * Check if the file loader Jar data table name is valid * * @return boolean */ protected final boolean hasJarDataTableName() { return m_jarDataTable != null ? true : false; } /** * Return the file loader Jar data table name * * @return String */ protected final String getJarDataTableName() { return m_jarDataTable; } /** * Check if the file loader queue table name is valid * * @return boolean */ protected final boolean hasQueueTableName() { return m_queueTable != null ? true : false; } /** * Return the file loader queue table name * * @return String */ protected final String getQueueTableName() { return m_queueTable; } /** * Check if the file loader transaction table name is valid * * @return boolean */ protected final boolean hasTransactionTableName() { return m_transactTable != null ? true : false; } /** * Return the file loader transaction table name * * @return String */ protected final String getTransactionTableName() { return m_transactTable; } /** * Check if the fileid/object id mapping table name is valid * * @return boolean */ protected final boolean hasObjectIdTableName() { return m_objectIdTable != null ? true : false; } /** * Return the fileid/object id mapping table name is valid * * @return String */ protected final String getObjectIdTableName() { return m_objectIdTable; } /** * Check if the symbolic links table name is valid * * @return boolean */ protected final boolean hasSymLinksTableName() { return m_symLinkTable != null ? true : false; } /** * Return the symbolic links table name is valid * * @return String */ protected final String getSymLinksTableName() { return m_symLinkTable; } /** * Return the data fragment size to store per BLOB when using the database to store * the file data. * * @return long */ protected final long getDataFragmentSize() { return m_dataFragSize; } /** * Check if database interface debug output is enabled * * @return boolean */ protected final boolean hasDebug() { return m_debug; } /** * Check if database interface SQL debug output is enabled * * @return boolean */ protected final boolean hasSQLDebug() { return m_sqlDebug; } /** * Set the DSN string * * @param dsn String */ protected final void setDSNString(String dsn) { m_dsn = dsn; } /** * Set the JDBC driver class * * @param driverClass String */ protected final void setDriverName(String driverClass) { m_driver = driverClass; } /** * Check for special characters within a file/directory name and escape the characters to return * a string which can be used in a SQL statement. * * @param name String * @return String */ protected String checkNameForSpecialChars(String name) { // Check for a null string if ( name == null || name.length() == 0) return name; // Check if the file/directory name contains special characters int idx = 0; boolean specChars = false; while ( idx < m_specialChars.length() && specChars == false) { // Check if the name contains the current special character if ( name.indexOf( m_specialChars.charAt(idx++)) != -1) specChars = true; } // Check if any special characters were found if ( specChars == false) return name; // Escape any special characters within the file/directory name StringBuffer nameBuf = new StringBuffer(name.length() * 2); for ( int i = 0; i < name.length(); i++) { // Get the current character and check if it needs to be escaped char curChar = name.charAt(i); // Append the character to the escape string if ( m_specialChars.indexOf(curChar) != -1) nameBuf.append("\\"); nameBuf.append(curChar); } // Return the escaped file/directory name return nameBuf.toString(); } /** * Create the database connection pool * * @exception Exception */ protected final void createConnectionPool() throws Exception { // Create the connection pool m_connPool = new DBConnectionPool(m_driver, m_dsn, m_userName, m_password, m_dbInitConns, m_dbMaxConns); // Set the online check interval, if specified if ( m_onlineCheckInterval != 0) m_connPool.setOnlineCheckInterval( m_onlineCheckInterval * 60); // Add the database interface as a connection pool event listener m_connPool.addConnectionPoolListener( this); } /** * Default implementation of the delete file request, throws an exception indicating that the * feature is not implemented. * * @param fileReq FileRequest * @throws DBException */ public void deleteFileRequest(FileRequest fileReq) throws DBException { // Indicate that the feature is not implemented throw new DBException("Feature not implemented"); } /** * Queue a file save request to the pending save queue as the database is offline * * @param saveReq FileRequest */ protected synchronized final void queueOfflineSaveRequest( FileRequest saveReq) { // Check if the offline queue is allocated if ( m_pendingSaveRequests == null) m_pendingSaveRequests = new FileRequestQueue(); // DEBUG if ( hasDebug()) Debug.println("JDBCInterface: Queueing save request " + saveReq); // Add the pending file save request m_pendingSaveRequests.addRequest( saveReq); } /** * Database online/offline status event * * @param dbonline boolean */ public void databaseOnlineStatus( boolean dbonline) { // DEBUG if ( hasDebug()) Debug.println( "JDBCInterface: Database connection event, status=" + ( dbonline ? "OnLine" : "OffLine")); // Set the shared device availabel status depending on the database state m_dbCtx.setAvailable( dbonline); // If the database is back online then check if there are queued save/delete requests if ( dbonline == true) { // Check if there are any queued delete file requests if ( m_dbCtx.hasOfflineFileDeletes()) { // Get the list of files to be deleted FileRequestQueue deleteList = m_dbCtx.getOfflineFileDeletes( true); for ( int i = 0; i < deleteList.numberOfRequests(); i++) { // Get the current delete file request DeleteFileRequest deleteReq = (DeleteFileRequest) deleteList.removeRequestNoWait(); // Delete the file record from the database try { // Check if the delete is for a file or stream if ( deleteReq.getStreamId() == 0) deleteFileRecord( -1, deleteReq.getFileId(), m_dbCtx.isTrashCanEnabled()); else deleteStreamRecord(deleteReq.getFileId(), deleteReq.getStreamId(), m_dbCtx.isTrashCanEnabled()); // Remove the file state for the file/stream m_dbCtx.getStateCache().removeFileState( deleteReq.getFileState().getPath()); // DEBUG if ( hasDebug()) Debug.println("JDBCInterface: Offline delete of file " + deleteReq.getVirtualPath() + ", fid=" + deleteReq.getFileId()); } catch ( Exception ex) { // Requeue the delete file request // TODO: } } } // Check if there are any queued file save requests if ( m_pendingSaveRequests != null) { // Unlink the pending save request list FileRequestQueue saveReqQueue = m_pendingSaveRequests; m_pendingSaveRequests = null; // DEBUG if ( hasDebug()) Debug.println( "JDBCInterface: Requeueing pending save requests, count=" + saveReqQueue.numberOfRequests()); // Queue the save requests FileRequest fileReq = null; while ( saveReqQueue.numberOfRequests() > 0) { // Get a request from the queue and requeue the request to save the data try { // Get the next save request to be requeued fileReq = saveReqQueue.removeRequestNoWait(); queueFileRequest( fileReq); // DEBUG if ( hasDebug()) Debug.println( "JDBCInterface: Requeued save " + fileReq); // Update the file size for the file being saved, the close method may not have updated the final file size // due to the database being offline if ( fileReq instanceof SingleFileRequest) { // Get the single file save request details SingleFileRequest singleReq = (SingleFileRequest) fileReq; if ( singleReq.hasFileState()) { // Get the temporary file size File tempFile = new File(singleReq.getTemporaryFile()); // Create the set file information details to set the file size FileInfo fInfo = new FileInfo( "", 0L, 0); fInfo.setFileInformationFlags( FileInfo.SetFileSize); fInfo.setFileId( singleReq.getFileId()); fInfo.setFileSize( tempFile.length()); // Update the file size in the database setFileInformation( -1, singleReq.getFileId(), fInfo); // DEBUG if ( hasDebug()) Debug.println( "JDBCInterface: Updated file size for " + singleReq.getVirtualPath() + " size=" + tempFile.length()); } } } catch ( DBException ex) { // If there was an error the request will go back onto a new in-memory pending queue } } } } } /** * Check if there are pending offline file requests queued * * @return boolean */ public final boolean hasOfflineFileRequests() { if ( m_pendingSaveRequests != null && m_pendingSaveRequests.numberOfRequests() > 0) return true; return false; } ///////////////////////////////////////////////////////////////////////////////////////////// /////////// Database interface methods required for threaded file loading support /////////// ///////////////////////////////////////////////////////////////////////////////////////////// /** * Default implementation of the queued request check, throws an exception indicating that the * feature is not implemented. * * @param tempFile String * @return boolean * @throws DBException */ public boolean hasQueuedRequest(String tempFile) throws DBException { // Indicate that the feature is not implemented throw new DBException("Feature not implemented"); } /** * Default implementation of load file requests, throws an exception indicating that the * feature is not implemented. * * @param seqNo int * @param reqType int * @param reqQueue FileRequestQueue * @param recLimit int * @return int * @throws DBException */ public int loadFileRequests(int seqNo, int reqType, FileRequestQueue reqQueue, int recLimit) throws DBException { // Indicate that the feature is not implemented throw new DBException("Feature not implemented"); } /** * Default implementation of queue file request, throws an exception indicating that the * feature is not implemented. * * @param fileReq FileRequest * @throws DBException */ public void queueFileRequest(FileRequest fileReq) throws DBException { // Indicate that the feature is not implemented throw new DBException("Feature not implemented"); } ///////////////////////////////////////////////////////////////////////////////////////////// /////////// Database interface methods required for threaded file loading /////////// /////////// transaction support /////////// ///////////////////////////////////////////////////////////////////////////////////////////// /** * Default implementation of the queue transaction request, throws an exception indicating that the * feature is not implemented. * * @param fileReq FileRequest * @exception DBException */ public void queueTransactionRequest(FileRequest fileReq) throws DBException { // Indicate that the feature is not implemented throw new DBException("Feature not implemented"); } /** * Default implementation of load transaction request, throws an exception indicating that the * feature is not implemented. * * @param tranReq MultipleFileRequest * @return MultipleFileRequest * @throws DBException */ public MultipleFileRequest loadTransactionRequest(MultipleFileRequest tranReq) throws DBException { // Indicate that the feature is not implemented throw new DBException("Feature not implemented"); } }