001    /*
002     * Copyright 2007 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 java.sql.Connection;
019    import java.sql.DatabaseMetaData;
020    import java.sql.ResultSet;
021    import java.sql.SQLException;
022    import java.sql.Statement;
023    import java.util.ArrayList;
024    import java.util.HashMap;
025    import java.util.List;
026    import java.util.Map;
027    
028    import javax.sql.DataSource;
029    
030    import junit.framework.Assert;
031    
032    import org.apache.commons.lang.StringUtils;
033    import org.apache.commons.lang.time.DurationFormatUtils;
034    import org.apache.commons.lang.time.StopWatch;
035    import org.apache.log4j.Logger;
036    import org.kuali.rice.core.api.config.property.ConfigContext;
037    import org.kuali.rice.core.api.lifecycle.BaseLifecycle;
038    import org.springframework.jdbc.core.ConnectionCallback;
039    import org.springframework.jdbc.core.JdbcTemplate;
040    import org.springframework.jdbc.core.StatementCallback;
041    import org.springframework.transaction.PlatformTransactionManager;
042    import org.springframework.transaction.TransactionStatus;
043    import org.springframework.transaction.support.TransactionCallback;
044    import org.springframework.transaction.support.TransactionTemplate;
045    
046    /**
047     * Lifecycle class to clean up the database for use in testing.
048     * This lifecycle will not be run (even if it is listed in the lifecycles list)
049     * if the 'use.use.clearDatabaseLifecycle' configuration property is defined, and is
050     * not 'true'.  If the property is omitted the lifecycle runs as normal.
051     * 
052     * @author Kuali Rice Team (rice.collab@kuali.org)
053     * @since 0.9
054     *
055     */
056    public class ClearDatabaseLifecycle extends BaseLifecycle {
057    
058        protected static final Logger LOG = Logger.getLogger(ClearDatabaseLifecycle.class);
059    
060        private List<String> tablesToClear = new ArrayList<String>();
061        private List<String> tablesNotToClear = new ArrayList<String>();
062    
063        public ClearDatabaseLifecycle() {
064            addStandardTables();
065        }
066    
067        public ClearDatabaseLifecycle(List<String> tablesToClear, List<String> tablesNotToClear) {
068            this.tablesToClear = tablesToClear;
069            this.tablesNotToClear = tablesNotToClear;
070            addStandardTables();
071        }
072    
073        protected void addStandardTables() {
074            tablesNotToClear.add("BIN.*");
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 DataSource dataSource) {
093            Assert.assertNotNull("DataSource could not be located.", dataSource);
094            try {
095                Connection connection = dataSource.getConnection();
096                connection.close();
097            } catch (Exception e) {
098                throw new RuntimeException(e);
099            }
100            return (Boolean) new JdbcTemplate(dataSource).execute(new ConnectionCallback() {
101                public Object doInConnection(final Connection connection) throws SQLException {
102                    final ResultSet resultSet = connection.getMetaData().getTables(null, connection.getMetaData().getUserName().toUpperCase(), TEST_TABLE_NAME, null);
103                    return new Boolean(resultSet.next());
104                }
105            });
106        }
107    
108        protected void verifyTestEnvironment(final DataSource dataSource) {
109            Assert.assertTrue("No table named '" + TEST_TABLE_NAME + "' was found in the configured database.  " + "You are attempting to run tests against a non-test database!!!", isTestTableInSchema(dataSource));
110        }
111    
112        protected void clearTables(final PlatformTransactionManager transactionManager, final DataSource dataSource) {
113            Assert.assertNotNull("DataSource could not be located.", dataSource);
114            try {
115                StopWatch s = new StopWatch();
116                s.start();
117                new TransactionTemplate(transactionManager).execute(new TransactionCallback() {
118                    public Object doInTransaction(final TransactionStatus status) {
119                        verifyTestEnvironment(dataSource);
120                        return new JdbcTemplate(dataSource).execute(new StatementCallback() {
121                            public Object doInStatement(Statement statement) throws SQLException {
122                                String schemaName = statement.getConnection().getMetaData().getUserName().toUpperCase();
123                                LOG.info("Clearing tables for schema " + schemaName);
124                                if (StringUtils.isBlank(schemaName)) {
125                                    Assert.fail("Empty schema name given");
126                                }
127                                final List<String> reEnableConstraints = new ArrayList<String>();
128                                DatabaseMetaData metaData = statement.getConnection().getMetaData();
129                                Map<String, List<String[]>> exportedKeys = indexExportedKeys(metaData, schemaName);
130                                final ResultSet resultSet = metaData.getTables(null, schemaName, null, new String[] { "TABLE" });
131                                final StringBuilder logStatements = new StringBuilder();
132                                while (resultSet.next()) {
133                                    String tableName = resultSet.getString("TABLE_NAME");
134                                    if (shouldTableBeCleared(tableName)) {
135                                        if (!isUsingDerby(metaData) && isUsingOracle(metaData)) {
136                                            List<String[]> exportedKeyNames = exportedKeys.get(tableName);
137                                            if (exportedKeyNames != null) {
138                                                    for (String[] exportedKeyName : exportedKeyNames) {
139                                                            final String fkName = exportedKeyName[0];
140                                                            final String fkTableName = exportedKeyName[1];
141                                                            final String disableConstraint = "ALTER TABLE " + fkTableName + " DISABLE CONSTRAINT " + fkName;
142                                                            logStatements.append("Disabling constraints using statement ->" + disableConstraint + "<-\n");
143                                                            statement.addBatch(disableConstraint);
144                                                            reEnableConstraints.add("ALTER TABLE " + fkTableName + " ENABLE CONSTRAINT " + fkName);
145                                                    }
146                                            }
147                                        } else if (isUsingMySQL(metaData)) {
148                                            statement.addBatch("SET FOREIGN_KEY_CHECKS = 0");
149                                        }
150                                        String deleteStatement = "DELETE FROM " + tableName;
151                                        logStatements.append("Clearing contents using statement ->" + deleteStatement + "<-\n");
152                                        statement.addBatch(deleteStatement);
153                                    }
154                                }
155                                for (final String constraint : reEnableConstraints) {
156                                    logStatements.append("Enabling constraints using statement ->" + constraint + "<-\n");
157                                    statement.addBatch(constraint);
158                                }
159                                if (isUsingMySQL(metaData)) {
160                                    statement.addBatch("SET FOREIGN_KEY_CHECKS = 1");
161                                }
162                                LOG.info(logStatements);
163                                
164                                int[] results = statement.executeBatch();
165                                for (int index = 0; index < results.length; index++) {
166                                    if (results[index] == Statement.EXECUTE_FAILED) {
167                                                    Assert.fail("Execution of database clear statement failed.");
168                                    }
169                                    
170                                }
171                                resultSet.close();
172                                LOG.info("Tables successfully cleared for schema " + schemaName);
173                                return null;
174                            }
175                        });
176                    }
177                });
178                s.stop();
179                LOG.info("Time to clear tables: " + DurationFormatUtils.formatDurationHMS(s.getTime()));
180            } catch (Exception e) {
181                LOG.error(e);
182                throw new RuntimeException(e);
183            }
184        }
185        
186        protected Map<String, List<String[]>> indexExportedKeys(DatabaseMetaData metaData, String schemaName) throws SQLException {
187            Map<String, List<String[]>> exportedKeys = new HashMap<String, List<String[]>>();
188            if (!isUsingDerby(metaData) && isUsingOracle(metaData)) {
189                    ResultSet keyResultSet = metaData.getExportedKeys(null, schemaName, null);
190                    while (keyResultSet.next()) {
191                            String tableName = keyResultSet.getString("PKTABLE_NAME");
192                            if (shouldTableBeCleared(tableName)) {
193                                    List<String[]> exportedKeyNames = exportedKeys.get(tableName);
194                                    if (exportedKeyNames == null) {
195                                            exportedKeyNames = new ArrayList<String[]>();
196                                            exportedKeys.put(tableName, exportedKeyNames);
197                                    }
198                                    final String fkName = keyResultSet.getString("FK_NAME");
199                                    final String fkTableName = keyResultSet.getString("FKTABLE_NAME");
200                                    exportedKeyNames.add(new String[] { fkName, fkTableName });
201                            }
202                    }
203                    keyResultSet.close();
204            }
205            return exportedKeys;        
206        }
207    
208        private boolean shouldTableBeCleared(String tableName) {
209            if (getTablesToClear() != null && !getTablesToClear().isEmpty()) {
210                for (String tableToClear : getTablesToClear()) {
211                    if (tableName.toUpperCase().matches(tableToClear.toUpperCase())) {
212                        return true;
213                    }
214                }
215                return false;
216            }
217            if (getTablesNotToClear() != null && !getTablesNotToClear().isEmpty()) {
218                for (String tableNotToClear : getTablesNotToClear()) {
219                    if (tableName.toUpperCase().matches(tableNotToClear.toUpperCase())) {
220                        return false;
221                    }
222                }
223            }
224            return true;
225        }
226    
227        private boolean isUsingDerby(DatabaseMetaData metaData) throws SQLException {
228            return metaData.getDriverName().toLowerCase().contains("derby");
229        }
230    
231        private boolean isUsingOracle(DatabaseMetaData metaData) throws SQLException {
232            return metaData.getDriverName().toLowerCase().contains("oracle");
233        }
234    
235        private boolean isUsingMySQL(DatabaseMetaData metaData) throws SQLException {
236            return metaData.getDriverName().toLowerCase().contains("mysql");
237        }
238    
239        
240        public List<String> getTablesToClear() {
241            return this.tablesToClear;
242        }
243    
244        public List<String> getTablesNotToClear() {
245            return this.tablesNotToClear;
246        }
247    }