001/** 002 * Copyright 2005-2014 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.test; 017 018import org.apache.commons.lang.StringUtils; 019import org.apache.commons.lang.time.DurationFormatUtils; 020import org.apache.commons.lang.time.StopWatch; 021import org.apache.log4j.Logger; 022import org.junit.Assert; 023import org.kuali.rice.core.api.config.property.ConfigContext; 024import org.kuali.rice.core.api.lifecycle.BaseLifecycle; 025import org.springframework.dao.DataAccessException; 026import org.springframework.jdbc.core.ConnectionCallback; 027import org.springframework.jdbc.core.JdbcTemplate; 028import org.springframework.jdbc.core.StatementCallback; 029import org.springframework.transaction.PlatformTransactionManager; 030import org.springframework.transaction.TransactionStatus; 031import org.springframework.transaction.support.TransactionCallback; 032import org.springframework.transaction.support.TransactionTemplate; 033 034import javax.sql.DataSource; 035import java.sql.Connection; 036import java.sql.DatabaseMetaData; 037import java.sql.ResultSet; 038import java.sql.SQLException; 039import java.sql.Statement; 040import java.util.ArrayList; 041import java.util.HashMap; 042import java.util.List; 043import java.util.Map; 044 045/** 046 * Lifecycle class to clean up the database for use in testing. 047 * This lifecycle will not be run (even if it is listed in the lifecycles list) 048 * if the 'use.use.clearDatabaseLifecycle' configuration property is defined, and is 049 * not 'true'. If the property is omitted the lifecycle runs as normal. 050 * 051 * @author Kuali Rice Team (rice.collab@kuali.org) 052 * @since 0.9 053 * 054 */ 055public class ClearDatabaseLifecycle extends BaseLifecycle { 056 057 protected static final Logger LOG = Logger.getLogger(ClearDatabaseLifecycle.class); 058 059 private List<String> tablesToClear = new ArrayList<String>(); 060 private List<String> tablesNotToClear = new ArrayList<String>(); 061 062 public ClearDatabaseLifecycle() { 063 addStandardTables(); 064 } 065 066 public ClearDatabaseLifecycle(List<String> tablesToClear, List<String> tablesNotToClear) { 067 this.tablesToClear = tablesToClear; 068 this.tablesNotToClear = tablesNotToClear; 069 addStandardTables(); 070 } 071 072 protected void addStandardTables() { 073 tablesNotToClear.add("BIN.*"); 074 tablesNotToClear.add(".*_S"); 075 } 076 077 public static final String TEST_TABLE_NAME = "EN_UNITTEST_T"; 078 079 public void start() throws Exception { 080 String useClearDatabaseLifecycle = ConfigContext.getCurrentContextConfig().getProperty("use.clearDatabaseLifecycle"); 081 082 if (useClearDatabaseLifecycle != null && !Boolean.valueOf(useClearDatabaseLifecycle)) { 083 LOG.debug("Skipping ClearDatabaseLifecycle due to property: use.clearDatabaseLifecycle=" + useClearDatabaseLifecycle); 084 return; 085 } 086 087 final DataSource dataSource = TestHarnessServiceLocator.getDataSource(); 088 clearTables(TestHarnessServiceLocator.getJtaTransactionManager(), dataSource); 089 super.start(); 090 } 091 092 protected Boolean isTestTableInSchema(final Connection connection) throws SQLException { 093 Assert.assertNotNull("Connection could not be located.", connection); 094 ResultSet resultSet = null; 095 try { 096 resultSet = connection.getMetaData().getTables(null, connection.getMetaData().getUserName().toUpperCase(), TEST_TABLE_NAME, null); 097 return new Boolean(resultSet.next()); 098 } finally { 099 if (resultSet != null) { 100 resultSet.close(); 101 } 102 } 103 } 104 105 protected void verifyTestEnvironment(final DataSource dataSource) { 106 new JdbcTemplate(dataSource).execute(new ConnectionCallback<Object>() { 107 public Object doInConnection(final Connection connection) throws SQLException { 108 String dbUrl = connection.getMetaData().getURL(); 109 Assert.assertTrue("No table named '" + TEST_TABLE_NAME + "' was found in the configured database. " + 110 dbUrl + " You are attempting to run tests against a non-test database!!!", isTestTableInSchema(connection)); 111 return null; 112 } 113 }); 114 } 115 116 protected void clearTables(final PlatformTransactionManager transactionManager, final DataSource dataSource) { 117 Assert.assertNotNull("DataSource could not be located.", dataSource); 118 try { 119 StopWatch s = new StopWatch(); 120 s.start(); 121 new TransactionTemplate(transactionManager).execute(new TransactionCallback() { 122 public Object doInTransaction(final TransactionStatus status) { 123 verifyTestEnvironment(dataSource); 124 return new JdbcTemplate(dataSource).execute(new StatementCallback() { 125 public Object doInStatement(Statement statement) throws SQLException { 126 String schemaName = statement.getConnection().getMetaData().getUserName().toUpperCase(); 127 LOG.info("Clearing tables for schema " + schemaName); 128 if (StringUtils.isBlank(schemaName)) { 129 Assert.fail("Empty schema name given"); 130 } 131 final List<String> reEnableConstraints = new ArrayList<String>(); 132 DatabaseMetaData metaData = statement.getConnection().getMetaData(); 133 Map<String, List<String[]>> exportedKeys = indexExportedKeys(metaData, schemaName); 134 final ResultSet resultSet = metaData.getTables(null, schemaName, null, new String[] { "TABLE" }); 135 final StringBuilder logStatements = new StringBuilder(); 136 while (resultSet.next()) { 137 String tableName = resultSet.getString("TABLE_NAME"); 138 if (shouldTableBeCleared(tableName)) { 139 if (!isUsingDerby(metaData) && isUsingOracle(metaData)) { 140 List<String[]> exportedKeyNames = exportedKeys.get(tableName); 141 if (exportedKeyNames != null) { 142 for (String[] exportedKeyName : exportedKeyNames) { 143 final String fkName = exportedKeyName[0]; 144 final String fkTableName = exportedKeyName[1]; 145 final String disableConstraint = "ALTER TABLE " + fkTableName + " DISABLE CONSTRAINT " + fkName; 146 logStatements.append("Disabling constraints using statement ->" + disableConstraint + "<-\n"); 147 statement.addBatch(disableConstraint); 148 reEnableConstraints.add("ALTER TABLE " + fkTableName + " ENABLE CONSTRAINT " + fkName); 149 } 150 } 151 } else if (isUsingMySQL(metaData)) { 152 statement.addBatch("SET FOREIGN_KEY_CHECKS = 0"); 153 } 154 String deleteStatement = "DELETE FROM " + tableName; 155 logStatements.append("Clearing contents using statement ->" + deleteStatement + "<-\n"); 156 statement.addBatch(deleteStatement); 157 } 158 } 159 for (final String constraint : reEnableConstraints) { 160 logStatements.append("Enabling constraints using statement ->" + constraint + "<-\n"); 161 statement.addBatch(constraint); 162 } 163 if (isUsingMySQL(metaData)) { 164 statement.addBatch("SET FOREIGN_KEY_CHECKS = 1"); 165 } 166 LOG.info(logStatements); 167 168 int[] results = statement.executeBatch(); 169 for (int index = 0; index < results.length; index++) { 170 if (results[index] == Statement.EXECUTE_FAILED) { 171 Assert.fail("Execution of database clear statement failed."); 172 } 173 174 } 175 resultSet.close(); 176 LOG.info("Tables successfully cleared for schema " + schemaName); 177 return null; 178 } 179 }); 180 } 181 }); 182 s.stop(); 183 LOG.info("Time to clear tables: " + DurationFormatUtils.formatDurationHMS(s.getTime())); 184 } catch (Exception e) { 185 LOG.error(e); 186 throw new RuntimeException(e); 187 } 188 } 189 190 protected Map<String, List<String[]>> indexExportedKeys(DatabaseMetaData metaData, String schemaName) throws SQLException { 191 Map<String, List<String[]>> exportedKeys = new HashMap<String, List<String[]>>(); 192 if (!isUsingDerby(metaData) && isUsingOracle(metaData)) { 193 ResultSet keyResultSet = metaData.getExportedKeys(null, schemaName, null); 194 while (keyResultSet.next()) { 195 String tableName = keyResultSet.getString("PKTABLE_NAME"); 196 if (shouldTableBeCleared(tableName)) { 197 List<String[]> exportedKeyNames = exportedKeys.get(tableName); 198 if (exportedKeyNames == null) { 199 exportedKeyNames = new ArrayList<String[]>(); 200 exportedKeys.put(tableName, exportedKeyNames); 201 } 202 final String fkName = keyResultSet.getString("FK_NAME"); 203 final String fkTableName = keyResultSet.getString("FKTABLE_NAME"); 204 exportedKeyNames.add(new String[] { fkName, fkTableName }); 205 } 206 } 207 keyResultSet.close(); 208 } 209 return exportedKeys; 210 } 211 212 private boolean shouldTableBeCleared(String tableName) { 213 if (getTablesNotToClear() != null && !getTablesNotToClear().isEmpty()) { 214 for (String tableNotToClear : getTablesNotToClear()) { 215 if (tableName.toUpperCase().matches(tableNotToClear.toUpperCase())) { 216 return false; 217 } 218 } 219 } 220 if (getTablesToClear() != null && !getTablesToClear().isEmpty()) { 221 for (String tableToClear : getTablesToClear()) { 222 if (tableName.toUpperCase().matches(tableToClear.toUpperCase())) { 223 return true; 224 } 225 } 226 return false; 227 } 228 229 return true; 230 } 231 232 private boolean isUsingDerby(DatabaseMetaData metaData) throws SQLException { 233 return metaData.getDriverName().toLowerCase().contains("derby"); 234 } 235 236 private boolean isUsingOracle(DatabaseMetaData metaData) throws SQLException { 237 return metaData.getDriverName().toLowerCase().contains("oracle"); 238 } 239 240 private boolean isUsingMySQL(DatabaseMetaData metaData) throws SQLException { 241 return metaData.getDriverName().toLowerCase().contains("mysql"); 242 } 243 244 245 public List<String> getTablesToClear() { 246 return this.tablesToClear; 247 } 248 249 public List<String> getTablesNotToClear() { 250 return this.tablesNotToClear; 251 } 252}