001 /**
002 * Copyright 2005-2011 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 */
016 package org.kuali.rice.test;
017
018 import org.apache.commons.lang.StringUtils;
019 import org.apache.commons.lang.time.DurationFormatUtils;
020 import org.apache.commons.lang.time.StopWatch;
021 import org.apache.log4j.Logger;
022 import org.junit.Assert;
023 import org.kuali.rice.core.api.config.property.ConfigContext;
024 import org.kuali.rice.core.api.lifecycle.BaseLifecycle;
025 import org.springframework.dao.DataAccessException;
026 import org.springframework.jdbc.core.ConnectionCallback;
027 import org.springframework.jdbc.core.JdbcTemplate;
028 import org.springframework.jdbc.core.StatementCallback;
029 import org.springframework.transaction.PlatformTransactionManager;
030 import org.springframework.transaction.TransactionStatus;
031 import org.springframework.transaction.support.TransactionCallback;
032 import org.springframework.transaction.support.TransactionTemplate;
033
034 import javax.sql.DataSource;
035 import java.sql.Connection;
036 import java.sql.DatabaseMetaData;
037 import java.sql.ResultSet;
038 import java.sql.SQLException;
039 import java.sql.Statement;
040 import java.util.ArrayList;
041 import java.util.HashMap;
042 import java.util.List;
043 import 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 */
055 public 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 }