001: /*
002: * Copyright 2006-2007, Unitils.org
003: *
004: * Licensed under the Apache 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.apache.org/licenses/LICENSE-2.0
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.unitils.database;
017:
018: import org.apache.commons.logging.Log;
019: import org.apache.commons.logging.LogFactory;
020: import org.unitils.core.Module;
021: import org.unitils.core.TestListener;
022: import org.unitils.core.Unitils;
023: import org.unitils.core.UnitilsException;
024: import org.unitils.core.dbsupport.SQLHandler;
025: import org.unitils.database.annotations.TestDataSource;
026: import org.unitils.database.annotations.Transactional;
027: import org.unitils.database.config.DataSourceFactory;
028: import org.unitils.database.transaction.TransactionManager;
029: import org.unitils.database.transaction.TransactionManagerFactory;
030: import org.unitils.database.transaction.TransactionalDataSource;
031: import org.unitils.database.util.Flushable;
032: import org.unitils.database.util.TransactionMode;
033: import static org.unitils.database.util.TransactionMode.*;
034:
035: import org.unitils.dbmaintainer.DBMaintainer;
036: import org.unitils.dbmaintainer.clean.DBCleaner;
037: import org.unitils.dbmaintainer.clean.DBClearer;
038: import org.unitils.dbmaintainer.structure.ConstraintsDisabler;
039: import org.unitils.dbmaintainer.structure.DataSetStructureGenerator;
040: import org.unitils.dbmaintainer.structure.SequenceUpdater;
041: import org.unitils.dbmaintainer.util.DatabaseModuleConfigUtils;
042: import org.unitils.dbmaintainer.util.DatabaseTask;
043:
044: import static org.unitils.util.AnnotationUtils.*;
045: import static org.unitils.util.ConfigUtils.getConfiguredInstance;
046: import static org.unitils.util.ModuleUtils.getAnnotationPropertyDefaults;
047: import static org.unitils.util.ModuleUtils.getEnumValueReplaceDefault;
048: import org.unitils.util.PropertyUtils;
049: import static org.unitils.util.ReflectionUtils.setFieldAndSetterValue;
050:
051: import javax.sql.DataSource;
052: import java.lang.annotation.Annotation;
053: import java.lang.reflect.Field;
054: import java.lang.reflect.Method;
055: import java.util.List;
056: import java.util.Map;
057: import java.util.Properties;
058:
059: /**
060: * todo add javadoc explaining transaction behavior.
061: * <p/>
062: * Module that provides basic support for database testing such as the creation of a datasource that connectes to the
063: * test database and the maintaince of the test database structure.
064: * <p/>
065: * A datasource will be created the first time one is requested. Which type of datasource will be created depends on
066: * the configured {@link DataSourceFactory}. By default this will be a pooled datasource that gets its connection-url
067: * and username and password from the unitils configuration.
068: * <p/>
069: * The created datasource can be injected into a field of the test by annotating the field with {@link TestDataSource}.
070: * It can then be used to install it in your DAO or other class under test. See the javadoc of the annotation for more info
071: * on how you can use it.
072: * <p/>
073: * If the DbMaintainer is enabled (by setting {@link #PROPKEY_UPDATEDATABASESCHEMA_ENABLED} to true), the test database
074: * schema will automatically be updated if needed. This check will be performed once during your test-suite run, namely
075: * when the data source is created. See {@link DBMaintainer} javadoc for more information on how this update is performed.
076: *
077: * @author Filip Neven
078: * @author Tim Ducheyne
079: */
080: public class DatabaseModule implements Module {
081:
082: /* Property indicating if the database schema should be updated before performing the tests */
083: public static final String PROPKEY_UPDATEDATABASESCHEMA_ENABLED = "updateDataBaseSchema.enabled";
084:
085: /* The logger instance for this class */
086: private static Log logger = LogFactory.getLog(DatabaseModule.class);
087:
088: /* Map holding the default configuration of the database module annotations */
089: private Map<Class<? extends Annotation>, Map<String, String>> defaultAnnotationPropertyValues;
090:
091: /* The datasource instance */
092: private TransactionalDataSource dataSource;
093:
094: /* The configuration of Unitils */
095: private Properties configuration;
096:
097: /* Indicates if the DBMaintainer should be invoked to update the database */
098: private boolean updateDatabaseSchemaEnabled;
099:
100: /* The transaction manager */
101: private TransactionManager transactionManager;
102:
103: /**
104: * Initializes this module using the given <code>Configuration</code>
105: *
106: * @param configuration the config, not null
107: */
108: public void init(Properties configuration) {
109: this .configuration = configuration;
110:
111: defaultAnnotationPropertyValues = getAnnotationPropertyDefaults(
112: DatabaseModule.class, configuration,
113: Transactional.class);
114: updateDatabaseSchemaEnabled = PropertyUtils.getBoolean(
115: PROPKEY_UPDATEDATABASESCHEMA_ENABLED, configuration);
116: }
117:
118: /**
119: * Returns the <code>DataSource</code> that provides connection to the unit test database. When invoked the first
120: * time, the DBMaintainer is invoked to make sure the test database is up-to-date (if database updating is enabled)
121: *
122: * @return The <code>DataSource</code>
123: */
124: public TransactionalDataSource getDataSource() {
125: if (dataSource == null) {
126: dataSource = createDataSource();
127: }
128: return dataSource;
129: }
130:
131: /**
132: * Gets the transaction manager or creates one if it does not exist yet.
133: *
134: * @return The transaction manager, not null
135: */
136: public TransactionManager getTransactionManager() {
137: if (transactionManager == null) {
138: transactionManager = createTransactionManager();
139: }
140: return transactionManager;
141: }
142:
143: /**
144: * Flushes all pending updates to the database. This method is useful when the effect of updates needs to
145: * be checked directly on the database.
146: * <p/>
147: * This will look for modules that implement {@link Flushable} and call flushDatabaseUpdates on these module.
148: * @param testObject TODO
149: */
150: public void flushDatabaseUpdates(Object testObject) {
151: logger.info("Flushing database updates.");
152: List<Flushable> flushables = Unitils.getInstance()
153: .getModulesRepository().getModulesOfType(
154: Flushable.class);
155: for (Flushable flushable : flushables) {
156: flushable.flushDatabaseUpdates(testObject);
157: }
158: }
159:
160: /**
161: * Determines whether the test database is outdated and, if that is the case, updates the database with the
162: * latest changes. See {@link DBMaintainer} for more information.
163: */
164: public void updateDatabase() {
165: updateDatabase(getDefaultSqlHandler());
166: }
167:
168: /**
169: * todo make configurable using properties
170: * <p/>
171: * Determines whether the test database is outdated and, if that is the case, updates the database with the
172: * latest changes. See {@link DBMaintainer} for more information.
173: *
174: * @param sqlHandler SQLHandler that needs to be used for the database updates
175: */
176: public void updateDatabase(SQLHandler sqlHandler) {
177: try {
178: logger.info("Checking if database has to be updated.");
179: DBMaintainer dbMaintainer = new DBMaintainer(configuration,
180: sqlHandler);
181: dbMaintainer.updateDatabase();
182:
183: } catch (UnitilsException e) {
184: throw new UnitilsException("Error while updating database",
185: e);
186: }
187: }
188:
189: /**
190: * Updates the database version to the current version, without issuing any other updates to the database.
191: * This method can be used for example after you've manually brought the database to the latest version, but
192: * the database version is not yet set to the current one. This method can also be useful for example for
193: * reinitializing the database after having reorganized the scripts folder.
194: */
195: public void setDatabaseToCurrentVersion() {
196: setDatabaseToCurrentVersion(getDefaultSqlHandler());
197: }
198:
199: /**
200: * Updates the database version to the current version, without issuing any other updates to the database.
201: * This method can be used for example after you've manually brought the database to the latest version, but
202: * the database version is not yet set to the current one. This method can also be useful for example for
203: * reinitializing the database after having reorganized the scripts folder.
204: *
205: * @param sqlHandler The {@link SQLHandler} to which all commands are issued
206: */
207: public void setDatabaseToCurrentVersion(SQLHandler sqlHandler) {
208: DBMaintainer dbMaintainer = new DBMaintainer(configuration,
209: sqlHandler);
210: dbMaintainer.setDatabaseToCurrentVersion();
211: }
212:
213: /**
214: * Assigns the <code>TestDataSource</code> to every field annotated with {@link TestDataSource} and calls all methods
215: * annotated with {@link TestDataSource}
216: *
217: * @param testObject The test instance, not null
218: */
219: public void injectDataSource(Object testObject) {
220: List<Field> fields = getFieldsAnnotatedWith(testObject
221: .getClass(), TestDataSource.class);
222: List<Method> methods = getMethodsAnnotatedWith(testObject
223: .getClass(), TestDataSource.class);
224: if (fields.isEmpty() && methods.isEmpty()) {
225: // Nothing to do. Jump out to make sure that we don't try to instantiate the DataSource
226: return;
227: }
228: setFieldAndSetterValue(testObject, fields, methods,
229: getDataSource());
230: }
231:
232: /**
233: * Creates a datasource by using the factory that is defined by the dataSourceFactory.className property
234: *
235: * @return the datasource
236: */
237: protected TransactionalDataSource createDataSource() {
238: // Get the factory for the data source and create it
239: DataSourceFactory dataSourceFactory = getConfiguredInstance(
240: DataSourceFactory.class, configuration);
241: dataSourceFactory.init(configuration);
242: DataSource dataSource = dataSourceFactory.createDataSource();
243:
244: // Make data source transactional
245: TransactionalDataSource transactionalDataSource = getTransactionManager()
246: .createTransactionalDataSource(dataSource);
247:
248: // Call the database maintainer if enabled
249: if (updateDatabaseSchemaEnabled) {
250: updateDatabase(new SQLHandler(transactionalDataSource));
251: }
252: return transactionalDataSource;
253: }
254:
255: /**
256: * @return An instance of the transactionManager, as configured in the Unitils configuration
257: */
258: protected TransactionManager createTransactionManager() {
259: // Get the factory for the transaction manager
260: TransactionManagerFactory transactionManagerFactory = getConfiguredInstance(
261: TransactionManagerFactory.class, configuration);
262: transactionManagerFactory.init(configuration);
263: return transactionManagerFactory.createTransactionManager();
264: }
265:
266: /**
267: * @param testObject The test object, not null
268: * @return The {@link TransactionMode} for the given object
269: */
270: protected TransactionMode getTransactionMode(Object testObject) {
271: TransactionMode transactionMode = getClassLevelAnnotationProperty(
272: Transactional.class, "value", DEFAULT, testObject
273: .getClass());
274: transactionMode = getEnumValueReplaceDefault(
275: Transactional.class, "value", transactionMode,
276: defaultAnnotationPropertyValues);
277: return transactionMode;
278: }
279:
280: /**
281: * Starts a transaction. If the Unitils DataSource was not loaded yet, we simply remember that a
282: * transaction was started but don't actually start it. If the DataSource is loaded within this
283: * test, the transaction will be started immediately after loading the DataSource.
284: *
285: * @param testObject The test object, not null
286: */
287: public void startTransaction(Object testObject) {
288: TransactionMode transactionMode = getTransactionMode(testObject);
289: if (transactionMode == DISABLED) {
290: return;
291: }
292: getTransactionManager().startTransaction(testObject);
293: }
294:
295: /**
296: * Commits or rollbacks the current transaction, if transactions are enabled and a transactionManager is
297: * active for the given testObject
298: *
299: * @param testObject The test object, not null
300: */
301: protected void endTransaction(Object testObject) {
302: TransactionMode transactionMode = getTransactionMode(testObject);
303: if (transactionMode == DISABLED) {
304: return;
305: }
306: TransactionManager transactionManager = getTransactionManager();
307: if (transactionMode == COMMIT) {
308: transactionManager.commit(testObject);
309: } else if (getTransactionMode(testObject) == ROLLBACK) {
310: transactionManager.rollback(testObject);
311: }
312: }
313:
314: /**
315: * Clears all configured schema's. I.e. drops all tables, views and other database objects.
316: */
317: public void clearSchemas() {
318: getConfiguredDatabaseTaskInstance(DBClearer.class)
319: .clearSchemas();
320: }
321:
322: /**
323: * Cleans all configured schema's. I.e. removes all data from its database tables.
324: */
325: public void cleanSchemas() {
326: getConfiguredDatabaseTaskInstance(DBCleaner.class)
327: .cleanSchemas();
328: }
329:
330: /**
331: * Disables all foreigh key and not-null constraints on the configured schema's.
332: */
333: public void disableConstraints() {
334: getConfiguredDatabaseTaskInstance(ConstraintsDisabler.class)
335: .disableConstraints();
336: }
337:
338: /**
339: * Updates all sequences that have a value below a certain configurable treshold to become equal
340: * to this treshold
341: */
342: public void updateSequences() {
343: getConfiguredDatabaseTaskInstance(SequenceUpdater.class)
344: .updateSequences();
345: }
346:
347: /**
348: * Generates a definition file that defines the structure of dataset's, i.e. a XSD of DTD that
349: * describes the structure of the database.
350: */
351: public void generateDatasetDefinition() {
352: getConfiguredDatabaseTaskInstance(
353: DataSetStructureGenerator.class)
354: .generateDataSetStructure();
355: }
356:
357: /**
358: * @return A configured instance of {@link DatabaseTask} of the given type
359: */
360: private <T extends DatabaseTask> T getConfiguredDatabaseTaskInstance(
361: Class<T> databaseTaskType) {
362: return DatabaseModuleConfigUtils
363: .getConfiguredDatabaseTaskInstance(databaseTaskType,
364: configuration, getDefaultSqlHandler());
365: }
366:
367: /**
368: * @return The default SQLHandler, which simply executes the sql statements on the unitils-configured
369: * test database
370: */
371: protected SQLHandler getDefaultSqlHandler() {
372: return new SQLHandler(getDataSource());
373: }
374:
375: /**
376: * @return The {@link TestListener} associated with this module
377: */
378: public TestListener createTestListener() {
379: return new DatabaseTestListener();
380: }
381:
382: /**
383: * The {@link TestListener} for this module
384: */
385: protected class DatabaseTestListener extends TestListener {
386:
387: @Override
388: public void beforeTestSetUp(Object testObject) {
389: injectDataSource(testObject);
390: startTransaction(testObject);
391: }
392:
393: @Override
394: public void afterTestTearDown(Object testObject) {
395: endTransaction(testObject);
396: }
397: }
398: }
|