/* * Copyright 2000-2013 Enonic AS * http://www.enonic.com/license */ package com.enonic.cms.upgrade.service; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.SingleConnectionDataSource; import org.springframework.transaction.support.TransactionCallback; import com.enonic.esl.sql.model.Database; import com.enonic.esl.sql.model.DatabaseSchemaTool; import com.enonic.esl.sql.model.Table; import com.enonic.esl.sql.model.View; import com.enonic.cms.framework.jdbc.dialect.Db2Dialect; import com.enonic.cms.framework.jdbc.dialect.Dialect; import com.enonic.cms.framework.jdbc.dialect.H2Dialect; import com.enonic.cms.framework.jdbc.dialect.OracleDialect; import com.enonic.cms.core.structure.SiteKey; import com.enonic.cms.store.DatabaseAccessor; import com.enonic.cms.upgrade.UpgradeContext; import com.enonic.cms.upgrade.log.UpgradeLog; public final class UpgradeContextImpl implements UpgradeContext { private final UpgradeLog log; private final SqlOperationHelper sqlHelper; private final int startModelNumber; private int currentModelNumber = -1; private final PropertyResolver propertyResolver; //default transaction timeout for upgrade is 24 hours //timeout in seconds: 24 hours (3600 * 24 = 86400) private int transactionTimeout = 86400; public UpgradeContextImpl( UpgradeLog log, PropertyResolver propertyResolver, SqlOperationHelper sqlHelper, int startModelNumber ) { this.log = log; this.sqlHelper = sqlHelper; this.startModelNumber = startModelNumber; this.propertyResolver = propertyResolver; } public Dialect getDialect() { return this.sqlHelper.getDialect(); } private Database getDatabase() { return DatabaseAccessor.getDatabase( currentModelNumber ); } public int getCurrentModelNumber() { return this.currentModelNumber; } public int getStartModelNumber() { return this.startModelNumber; } public void setCurrentModelNumber( int currentModelNumber ) { this.currentModelNumber = currentModelNumber; } public void updateModelNumber( int modelNumber ) throws Exception { this.sqlHelper.updateModelNumber( modelNumber ); } public void logInfo( String message ) { this.log.logInfo( this.currentModelNumber, message ); } public void logWarning( String message ) { this.log.logWarning( this.currentModelNumber, message ); } public void logWarning( String message, Throwable cause ) { this.log.logWarning( this.currentModelNumber, message, cause ); } public void logError( String message ) { this.log.logError( this.currentModelNumber, message ); } public void logError( String message, Throwable cause ) { this.log.logError( this.currentModelNumber, message, cause ); } public void createTable( String tableName ) throws Exception { final Table table = this.getDatabase().getTable( tableName ); executeStatement( DatabaseSchemaTool.generateCreateTable( table ), false ); // creates both foreign keys and indexes createTableConstraints( tableName, true ); } public void createViews( String... viewNames ) throws Exception { for ( String viewName : viewNames ) { final View view = this.getDatabase().getView( viewName ); executeStatement( DatabaseSchemaTool.generateCreateView( view ), false ); } } private void createTableIndexes( String tableName, boolean logSql ) throws Exception { final List<String> statements = DatabaseSchemaTool.generateCreateIndexes( this.getDatabase().getTable( tableName ) ); executeStatements( statements, logSql ); } private void dropTableIndexes( String tableName, boolean logSql ) throws Exception { Connection conn = null; try { conn = getConnection(); DatabaseMetaData metaData = conn.getMetaData(); Set<String> primaryKeys = getPrimaryKeys( metaData, tableName ); Collection<String> indexNames = getIndexNames( metaData, tableName, primaryKeys ); for ( String indexName : indexNames ) { String sql = getDialect().translateDropIndex( tableName, indexName ); executeStatement( sql, logSql ); } } finally { close( conn ); } } private void createTableForeignKeys( String tableName, boolean logSql ) throws Exception { executeStatements( DatabaseSchemaTool.generateCreateForeignKeys( this.getDatabase().getTable( tableName ) ), logSql ); } public void createTableConstraints( String tableName, boolean logSql ) throws Exception { createTableIndexes( tableName, logSql ); createTableForeignKeys( tableName, logSql ); } public void dropTableConstraints( String tableName, boolean logSql ) throws Exception { /* Dropping foreign keys before indexes. Some databases always create a matching index for each foreign key */ dropTableForeignKeys( tableName, logSql ); dropTableIndexes( tableName, logSql ); } private void dropTableForeignKeys( String tableName, boolean logSql ) throws Exception { Connection conn = null; try { conn = getConnection(); DatabaseMetaData metaData = conn.getMetaData(); Collection<String> keys = getForeignKeys( metaData, tableName ); for ( String key : keys ) { String sql = getDialect().translateDropForeignKey( tableName, key ); executeStatement( sql, logSql ); } } finally { close( conn ); } } public void dropTable( String tableName ) throws Exception { executeStatement( "DROP TABLE " + tableName, false ); } public void dropColumn( String tableName, String columnName ) throws Exception { executeStatement( "ALTER TABLE " + tableName + " DROP COLUMN " + columnName, false ); } public void dropView( String viewName ) throws Exception { executeStatement( "DROP VIEW " + viewName, false ); } public void dropViews( String... views ) throws Exception { for ( String view : views ) { dropView( view ); } } public void createNewLastKey( String tableName, int lastKey ) throws SQLException { getJdbcTemplate().execute( "INSERT INTO tKey (key_sTableName, key_lLastKey) VALUES ('" + tableName.toLowerCase() + "', " + lastKey + ")" ); } public int executeUpdate( String statement, Object[] args ) throws Exception { return getJdbcTemplate().update( statement, args ); } public int executeUpdate( String statement, Object[] args, int[] jdbcTypes ) throws Exception { return getJdbcTemplate().update( statement, args, jdbcTypes ); } private void executeStatement( String sql, boolean logSql ) throws Exception { Connection conn = null; Statement stmt = null; try { if ( logSql ) { logInfo( sql ); } conn = getConnection(); stmt = conn.createStatement(); stmt.execute( sql ); } finally { close( stmt ); close( conn ); } } private void executeStatements( List list, boolean logSql ) throws Exception { for ( Object value : list ) { String sql = (String) value; executeStatement( sql, logSql ); } } public Connection getConnection() throws SQLException { return this.sqlHelper.getConnection(); } public JdbcTemplate getJdbcTemplate() throws SQLException { return new JdbcTemplate( new SingleConnectionDataSource( getConnection(), true ) ); } public void close( Connection conn ) { this.sqlHelper.close( conn ); } public void close( Statement stmt ) { this.sqlHelper.close( stmt ); } public void close( ResultSet result ) { this.sqlHelper.close( result ); } private Set<String> getPrimaryKeys( DatabaseMetaData metaData, String tableName ) throws SQLException { Set<String> pks = new HashSet<String>(); ResultSet resultSet = null; try { resultSet = metaData.getPrimaryKeys( null, getCurrentSchema(), tableName ); while ( resultSet.next() ) { String name = resultSet.getString( "PK_NAME" ); if ( name != null ) { pks.add( name ); } } } finally { if ( resultSet != null ) { resultSet.close(); } } return pks; } private Collection<String> getIndexNames( DatabaseMetaData metaData, String tableName, Set<String> primaryKeys ) throws SQLException { Set<String> indexNames = new HashSet<String>(); ResultSet resultSet = null; try { final String currentSchema = getCurrentSchema(); final String table = getTableName( metaData, tableName ); resultSet = metaData.getIndexInfo( null, null, table, false, false ); while ( resultSet.next() ) { String name = resultSet.getString( "INDEX_NAME" ); boolean nonUnique = resultSet.getBoolean( "NON_UNIQUE" ); String qualifier = resultSet.getString( "INDEX_QUALIFIER" ); if ( isOracleDatabase() && !currentSchema.equalsIgnoreCase( resultSet.getString( "TABLE_SCHEM" ) ) ) { continue; } if ( isH2Database() ) { // Ignore qualifier on H2 database (should be placed inside dialect) qualifier = null; } if ( name != null ) { if ( name.toLowerCase().startsWith( "idx_" ) ) { indexNames.add( name ); } else { // Do we need this? Haven't all indexes been added with the previous statement? if ( qualifier != null && qualifier.trim().length() > 0 ) { /* some databases give empty ("") qualifier */ name = qualifier + "." + name; } if ( !primaryKeys.contains( name ) && nonUnique ) { indexNames.add( name ); } } } } } finally { if ( resultSet != null ) { resultSet.close(); } } return indexNames; } private Set<String> getForeignKeys( DatabaseMetaData metaData, String tableName ) throws SQLException { Set<String> keys = new HashSet<String>(); ResultSet resultSet = null; try { resultSet = metaData.getImportedKeys( null, getCurrentSchema(), getTableName( metaData, tableName ) ); while ( resultSet.next() ) { String name = resultSet.getString( "FK_NAME" ); if ( name != null ) { keys.add( name ); } } } finally { if ( resultSet != null ) { resultSet.close(); } } return keys; } private String getTableName( DatabaseMetaData metaData, String tableName ) throws SQLException { ResultSet resultSet = null; try { resultSet = metaData.getTables( null, getCurrentSchema(), "%", null ); while ( resultSet.next() ) { String name = resultSet.getString( "TABLE_NAME" ); if ( name != null && name.equalsIgnoreCase( tableName ) ) { return name; } } } finally { if ( resultSet != null ) { resultSet.close(); } } return tableName; } public void reorganizeTablesForDb2( String... tables ) throws Exception { if ( !isDb2Database() ) { return; } logWarning( "Db2Dialect: Reorganizing tables" ); // Below code illustrates that it is possible to automate this (find tables that needs reorg) : // Create generalt "cleanup" prcedure by selecting tables that requires reorg // select TABSCHEMA, TABNAME from SYSIBMADM.ADMINTABINFO where REORG_PENDING = 'Y' // CALL SYSPROC.ADMIN_CMD('reorg table <TABSCHEMA>.<TABNAME>') for ( String table : tables ) { getJdbcTemplate().execute( "CALL SYSPROC.ADMIN_CMD('reorg table " + table + "')" ); } } private String getCurrentSchema() throws SQLException { if ( isOracleDatabase() ) { // Only Oracle (as far as we know) needs schema to differ from other schemas when the user as rights across // schemas. List rows = getJdbcTemplate().queryForList( "select sys_context( 'userenv', 'current_schema' ) as s from dual", String.class ); if ( rows.size() > 0 ) { return (String) rows.get( 0 ); } } return null; } private boolean isDb2Database() { return ( getDialect() instanceof Db2Dialect ); } private boolean isOracleDatabase() { return ( getDialect() instanceof OracleDialect ); } private boolean isH2Database() { return ( getDialect() instanceof H2Dialect ); } public void dropViews( boolean logSql ) throws Exception { executeStatements( DatabaseSchemaTool.generateDropViews( getDatabase() ), logSql ); } public void createViews( boolean logSql ) throws Exception { executeStatements( DatabaseSchemaTool.generateCreateViews( getDatabase() ), logSql ); } public String getProperty( String name ) { return this.propertyResolver.getProperty( name ); } public String getConfigDirPath() { return this.propertyResolver.getConfigDirPath(); } public String getProperty( SiteKey siteKey, String name ) { return this.propertyResolver.getProperty( siteKey, name ); } public int generateNextKey( String tableName ) throws Exception { return this.sqlHelper.generateNextKey( tableName ); } // public Object execute( TransactionCallback callback ) // { // return this.sqlHelper.execute( callback ); // } public Object execute( TransactionCallback callback ) { final int defaultTimeout = sqlHelper.getTransactionTimeout(); sqlHelper.setTransactionTimeout( transactionTimeout ); Object object = this.sqlHelper.execute( callback ); sqlHelper.setTransactionTimeout( defaultTimeout ); return object; } public int getTransactionTimeout() { return transactionTimeout; } public List<String> getStatementsFromSchema( final int modelNumber ) { return DatabaseAccessor.getDatabase( modelNumber ).getStatements(); } public void executeStatement( final String statement ) throws Exception { executeStatement( statement, true ); } public boolean hasTable( final String table ) throws Exception { return this.sqlHelper.hasTable( getConnection(), table ); } public boolean columnExist( final String tableName, final String columnName ) throws Exception { return this.sqlHelper.columnExist( getConnection(), tableName, columnName ); } }