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 */
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}