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    }