/**
* The contents of this file are subject to the OpenMRS Public License
* Version 1.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* http://license.openmrs.org
*
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
* License for the specific language governing rights and limitations
* under the License.
*
* Copyright (C) OpenMRS, LLC. All Rights Reserved.
*/
package org.openmrs.test;
import java.awt.Font;
import java.awt.Frame;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Window;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JTextField;
import javax.swing.UIManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dbunit.database.DatabaseConfig;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.DefaultDataSet;
import org.dbunit.dataset.DefaultTable;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ReplacementDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.dataset.xml.XmlDataSet;
import org.dbunit.ext.hsqldb.HsqldbDataTypeFactory;
import org.dbunit.operation.DatabaseOperation;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Environment;
import org.hibernate.dialect.H2Dialect;
import org.hibernate.impl.SessionFactoryImpl;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.metadata.CollectionMetadata;
import org.hibernate.persister.collection.CollectionPersister;
import org.hibernate.persister.entity.EntityPersister;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.openmrs.api.context.Context;
import org.openmrs.api.context.ContextAuthenticationException;
import org.openmrs.module.ModuleConstants;
import org.openmrs.module.ModuleUtil;
import org.openmrs.util.OpenmrsClassLoader;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
import org.springframework.transaction.annotation.Transactional;
/**
* This is the base for spring/context tests. Tests that NEED to use calls to the Context class and
* use Services and/or the database should extend this class. NOTE: Tests that do not need access to
* spring enabled services do not need this class and extending this will only slow those test cases
* down. (because spring is started before test cases are run). Normal test cases do not need to
* extend anything
*/
@ContextConfiguration(locations = { "classpath:applicationContext-service.xml", "classpath*:openmrs-servlet.xml",
"classpath*:moduleApplicationContext.xml" })
@TestExecutionListeners( { TransactionalTestExecutionListener.class, SkipBaseSetupAnnotationExecutionListener.class,
StartModuleExecutionListener.class })
@Transactional
public abstract class BaseContextSensitiveTest extends AbstractJUnit4SpringContextTests {
private static Log log = LogFactory.getLog(BaseContextSensitiveTest.class);
/**
* Only the classpath/package path and filename of the initial dataset
*/
protected static final String INITIAL_XML_DATASET_PACKAGE_PATH = "org/openmrs/include/initialInMemoryTestDataSet.xml";
protected static final String EXAMPLE_XML_DATASET_PACKAGE_PATH = "org/openmrs/include/standardTestDataset.xml";
/**
* cached runtime properties
*/
protected static Properties runtimeProperties;
/**
* Used for username/password dialog
*/
private static final Font font = new Font("Arial", Font.BOLD, 16);
/**
* Our username field is outside of the getUsernameAndPassword() method so we can do our
* force-focus-on-the-username-field trick -- i.e., refer to the field within an anonymous
* TimerTask method.
*/
private static JTextField usernameField;
/**
* This frame contains the password dialog box. In order to bring the frame to the front in the
* TimerTask method, we make it a private field
*/
private static Frame frame;
/**
* Static variable to keep track of the number of times this class has been loaded (aka, number
* of tests already run)
*/
private static Integer loadCount = 0;
/**
* Basic constructor for the super class to all openmrs api unit tests. This constructor sets up
* the classloader and the properties file so that by the type spring gets around to finally
* starting, the openmrs runtime properties are already in place A static load count is kept to
* count the number of times this class has been loaded.
*
* @see #getLoadCount()
*/
public BaseContextSensitiveTest() {
Thread.currentThread().setContextClassLoader(OpenmrsClassLoader.getInstance());
Properties props = getRuntimeProperties();
if (log.isDebugEnabled())
log.debug("props: " + props);
Context.setRuntimeProperties(props);
loadCount++;
}
/**
* Modules should extend {@link BaseModuleContextSensitiveTest}, not this class. If they extend
* this class, then they won't work right when run in batches.
*
* @throws Exception
*/
@Before
public void checkNotModule() throws Exception {
if (this.getClass().getPackage().toString().contains("org.openmrs.module.")
&& !(this instanceof BaseModuleContextSensitiveTest)) {
throw new RuntimeException(
"Module unit test classes should extend BaseModuleContextSensitiveTest, not just BaseContextSensitiveTest");
}
}
/**
* Get the number of times this class has been loaded. This is a rough approx of how many tests
* have been run so far. This can be used to determine if the test is being run in a standalone
* context or if other tests have been run before.
*
* @return number of times this class has been loaded
*/
public Integer getLoadCount() {
return loadCount;
}
/**
* Used for runtime properties. The default is "openmrs" because most people will use that as
* the default. If your webapp and runtime properties are under a different name, override this
* method in your tests
*
* @return String webapp name to assume when looking up the runtime properties
*/
public String getWebappName() {
return "openmrs";
}
/**
* Mimics org.openmrs.web.Listener.getRuntimeProperties() Overrides the database connection
* properties if the user wants an in-memory database
*
* @return Properties runtime
*/
public Properties getRuntimeProperties() {
// cache the properties for subsequent calls
if (runtimeProperties == null)
runtimeProperties = TestUtil.getRuntimeProperties(getWebappName());
// if we're using the in-memory hypersonic database, add those
// connection properties here to override what is in the runtime
// properties
if (useInMemoryDatabase() == true) {
runtimeProperties.setProperty(Environment.DIALECT, H2Dialect.class.getName());
runtimeProperties.setProperty(Environment.URL, "jdbc:h2:mem:openmrs;DB_CLOSE_DELAY=30");
runtimeProperties.setProperty(Environment.DRIVER, "org.h2.Driver");
runtimeProperties.setProperty(Environment.USER, "sa");
runtimeProperties.setProperty(Environment.PASS, "");
// these two properties need to be set in case the user has this exact
// phrasing in their runtime file.
runtimeProperties.setProperty("connection.username", "sa");
runtimeProperties.setProperty("connection.password", "");
// automatically create the tables defined in the hbm files
runtimeProperties.setProperty(Environment.HBM2DDL_AUTO, "create-drop");
}
// we don't want to try to load core modules in tests
runtimeProperties.setProperty(ModuleConstants.IGNORE_CORE_MODULES_PROPERTY, "true");
return runtimeProperties;
}
/**
* Authenticate to the Context. A popup box will appear asking the current user to enter
* credentials unless there is a junit.username and junit.password defined in the runtime
* properties
*
* @throws Exception
*/
public void authenticate() throws Exception {
if (Context.isAuthenticated())
return;
try {
Context.authenticate("admin", "test");
return;
}
catch (ContextAuthenticationException wrongCredentialsError) {
if (useInMemoryDatabase()) {
// if we get here the user is using some database other than the standard
// in-memory database, prompt the user for input
log.error("For some reason we couldn't auth as admin:test ?!", wrongCredentialsError);
}
}
Integer attempts = 0;
// TODO: how to make this a locale specific message for the user to see?
String message = null;
// only need to authenticate once per session
while (Context.isAuthenticated() == false && attempts < 3) {
// look in the runtime properties for a defined username and
// password first
String junitusername = null;
String junitpassword = null;
try {
Properties props = this.getRuntimeProperties();
junitusername = props.getProperty("junit.username");
junitpassword = props.getProperty("junit.password");
}
catch (Exception e) {
// if anything happens just default to asking the user
}
String[] credentials = null;
// ask the user for creds if no junit username/pass defined
// in the runtime properties or if that username/pass failed already
if (junitusername == null || junitpassword == null || attempts > 0) {
credentials = askForUsernameAndPassword(message);
// credentials are null if the user clicked "cancel" in popup
if (credentials == null)
return;
} else
credentials = new String[] { junitusername, junitpassword };
// try to authenticate to the Context with either the runtime
// defined credentials or the user supplied credentials from the
// popup
try {
Context.authenticate(credentials[0], credentials[1]);
}
catch (ContextAuthenticationException e) {
message = "Invalid username/password. Try again.";
}
attempts++;
}
}
/**
* Utility method for obtaining username and password through Swing interface for tests. Any
* tests extending the org.openmrs.BaseTest class may simply invoke this method by name.
* Username and password are returned in a two-member String array. If the user aborts, null is
* returned. <b> <em>Do not call for non-interactive tests, since this method will try to
* render an interactive dialog box for authentication!</em></b>
*
* @param message string to display above username field
* @return Two-member String array containing username and password, respectively, or
* <code>null</code> if user aborts dialog
*/
public static synchronized String[] askForUsernameAndPassword(String message) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
}
catch (Exception e) {
}
if (message == null || "".equals(message))
message = "Enter username/password to authenticate to OpenMRS...";
JPanel panel = new JPanel(new GridBagLayout());
JLabel usernameLabel = new JLabel("Username");
usernameLabel.setFont(font);
usernameField = new JTextField(20);
usernameField.setFont(font);
JLabel passwordLabel = new JLabel("Password");
passwordLabel.setFont(font);
JPasswordField passwordField = new JPasswordField(20);
passwordField.setFont(font);
panel.add(usernameLabel, new GridBagConstraints(0, 0, 1, 1, 0, 0, GridBagConstraints.EAST,
GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 5, 0));
panel.add(usernameField, new GridBagConstraints(1, 0, 1, 1, 0, 0, GridBagConstraints.WEST,
GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
panel.add(passwordLabel, new GridBagConstraints(0, 1, 1, 1, 0, 0, GridBagConstraints.EAST,
GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 5, 0));
panel.add(passwordField, new GridBagConstraints(1, 1, 1, 1, 0, 0, GridBagConstraints.WEST,
GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
frame = new JFrame();
Window window = new Window(frame);
frame.setVisible(true);
frame.setTitle("JUnit Test Credentials");
// We use a TimerTask to force focus on username, but still use
// JOptionPane for model dialog
TimerTask later = new TimerTask() {
@Override
public void run() {
if (frame != null) {
// bring the dialog's window to the front
frame.toFront();
usernameField.grabFocus();
}
}
};
// try setting focus half a second from now
new Timer().schedule(later, 500);
// attention grabber for those people that aren't as observant
TimerTask laterStill = new TimerTask() {
@Override
public void run() {
if (frame != null) {
frame.toFront(); // bring the dialog's window to the
// front
usernameField.grabFocus();
}
}
};
// if the user hasn't done anything in 10 seconds, tell them the window
// is there
new Timer().schedule(laterStill, 10000);
// show the dialog box
int response = JOptionPane.showConfirmDialog(window, panel, message, JOptionPane.OK_CANCEL_OPTION);
// clear out the window so the timer doesn't screw up
laterStill.cancel();
frame.setVisible(false);
window.setVisible(false);
frame = null;
// response of 2 is the cancel button, response of -1 is the little red
// X in the top right
return (response == 2 || response == -1 ? null : new String[] { usernameField.getText(),
String.valueOf(passwordField.getPassword()) });
}
/**
* Override this method to turn on/off the in-memory database. The default is to use the
* in-memory database. When this method returns false, the database defined by the runtime
* properties is used instead
*
* @return true/false whether or not to use an in memory database
*/
public Boolean useInMemoryDatabase() {
return true;
}
/**
* Get the database connection currently in use by the testing framework
*
* @return Connection jdbc connection to the database
*/
@SuppressWarnings("deprecation")
public Connection getConnection() {
SessionFactory sessionFactory = (SessionFactory) applicationContext.getBean("sessionFactory");
return sessionFactory.getCurrentSession().connection();
}
/**
* This initializes the empty in-memory hsql database with some rows in order to actually run
* some tests
*/
public void initializeInMemoryDatabase() throws Exception {
// don't allow the user to overwrite their data
if (useInMemoryDatabase() == false)
throw new Exception(
"You shouldn't be initializing a NON in-memory database. Consider unoverriding useInMemoryDatabase");
executeDataSet(INITIAL_XML_DATASET_PACKAGE_PATH);
}
/**
* Used by {@link #executeDataSet(String)} to cache the parsed xml files. This speeds up
* subsequent runs of the dataset
*/
private static Map<String, IDataSet> cachedDatasets = new HashMap<String, IDataSet>();
/**
* Runs the flat xml data file at the classpath location specified by
* <code>datasetFilename</code> This is a convenience method. It simply creates an
* {@link IDataSet} and calls {@link #executeDataSet(IDataSet)}
*
* @param datasetFilename String path/filename on the classpath of the xml data set to clean
* insert into the current database
* @see #getConnection()
* @see #executeDataSet(IDataSet)
*/
public void executeDataSet(String datasetFilename) throws Exception {
// try to get the given filename from the cache
IDataSet xmlDataSetToRun = cachedDatasets.get(datasetFilename);
// if we didn't find it in the cache, load it
if (xmlDataSetToRun == null) {
File file = new File(datasetFilename);
InputStream fileInInputStreamFormat = null;
// try to load the file if its a straight up path to the file or
// if its a classpath path to the file
if (file.exists())
fileInInputStreamFormat = new FileInputStream(datasetFilename);
else {
fileInInputStreamFormat = getClass().getClassLoader().getResourceAsStream(datasetFilename);
if (fileInInputStreamFormat == null)
throw new FileNotFoundException("Unable to find '" + datasetFilename + "' in the classpath");
}
Reader reader = null;
try {
reader = new InputStreamReader(fileInInputStreamFormat);
ReplacementDataSet replacementDataSet = new ReplacementDataSet(
new FlatXmlDataSet(reader, false, true, false));
replacementDataSet.addReplacementObject("[NULL]", null);
xmlDataSetToRun = replacementDataSet;
}
finally {
fileInInputStreamFormat.close();
if (reader != null)
reader.close();
}
// cache the xmldataset for future runs of this file
cachedDatasets.put(datasetFilename, xmlDataSetToRun);
}
executeDataSet(xmlDataSetToRun);
}
/**
* Runs the xml data file at the classpath location specified by <code>datasetFilename</code>
* using XmlDataSet. It simply creates an {@link IDataSet} and calls
* {@link #executeDataSet(IDataSet)}. <br/>
* <br/>
* This method is different than {@link #executeDataSet(String)} in that this one does not
* expect a flat file xml but instead a true XmlDataSet. <br/>
* <br/>
* In addition, there is no replacing of [NULL] values in strings.
*
* @param datasetFilename String path/filename on the classpath of the xml data set to clean
* insert into the current database
* @see #getConnection()
* @see #executeDataSet(IDataSet)
*/
public void executeXmlDataSet(String datasetFilename) throws Exception {
// try to get the given filename from the cache
IDataSet xmlDataSetToRun = cachedDatasets.get(datasetFilename);
// if we didn't find it in the cache, load it
if (xmlDataSetToRun == null) {
File file = new File(datasetFilename);
InputStream fileInInputStreamFormat = null;
// try to load the file if its a straight up path to the file or
// if its a classpath path to the file
if (file.exists())
fileInInputStreamFormat = new FileInputStream(datasetFilename);
else {
fileInInputStreamFormat = getClass().getClassLoader().getResourceAsStream(datasetFilename);
if (fileInInputStreamFormat == null)
throw new FileNotFoundException("Unable to find '" + datasetFilename + "' in the classpath");
}
XmlDataSet xmlDataSet = null;
try {
xmlDataSet = new XmlDataSet(fileInInputStreamFormat);
xmlDataSetToRun = xmlDataSet;
}
finally {
fileInInputStreamFormat.close();
}
// cache the xmldataset for future runs of this file
cachedDatasets.put(datasetFilename, xmlDataSetToRun);
}
executeDataSet(xmlDataSetToRun);
}
/**
* Run the given dataset specified by the <code>dataset</code> argument
*
* @param dataset IDataSet to run on the current database used by Spring
* @see #getConnection()
*/
public void executeDataSet(IDataSet dataset) throws Exception {
Connection connection = getConnection();
// convert the current session's connection to a dbunit connection
IDatabaseConnection dbUnitConn = new DatabaseConnection(connection);
// turn off the database constraints
if (useInMemoryDatabase()) {
// use the hsql datatypefactory so that boolean properties work correctly
DatabaseConfig config = dbUnitConn.getConfig();
config.setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, new HsqldbDataTypeFactory());
// for the hsql database
String sql = "SET REFERENTIAL_INTEGRITY FALSE";
PreparedStatement ps = connection.prepareStatement(sql);
ps.execute();
ps.close();
} else {
// for the mysql database
String sql = "SET FOREIGN_KEY_CHECKS=0;";
PreparedStatement ps = connection.prepareStatement(sql);
ps.execute();
ps.close();
}
// do the actual update/insert:
// insert new rows, update existing rows, and leave others alone
DatabaseOperation.REFRESH.execute(dbUnitConn, dataset);
//turn foreign key checks back on
if (useInMemoryDatabase()) {
// for the hsql database
String sql = "SET REFERENTIAL_INTEGRITY TRUE";
PreparedStatement ps = connection.prepareStatement(sql);
ps.execute();
ps.close();
} else {
// for the mysql db
String sql = "SET FOREIGN_KEY_CHECKS=1;";
PreparedStatement ps = connection.prepareStatement(sql);
ps.execute();
ps.close();
}
}
/**
* This is a convenience method to clear out all rows in all tables in the current connection
*
* @throws Exception
*/
@After
public void deleteAllData() throws Exception {
Connection connection = getConnection();
// convert the current session's connection to a dbunit connection
IDatabaseConnection dbUnitConn = new DatabaseConnection(connection);
// turn off the database constraints so we can delete tables willy-nilly
if (useInMemoryDatabase()) {
// for the hsql database
String sql = "SET REFERENTIAL_INTEGRITY FALSE";
PreparedStatement ps = connection.prepareStatement(sql);
ps.execute();
ps.close();
}
// find all the tables for this connection
ResultSet resultSet = connection.getMetaData().getTables(null, "PUBLIC", "%", null);
DefaultDataSet dataset = new DefaultDataSet();
while (resultSet.next()) {
String tableName = resultSet.getString(3);
dataset.addTable(new DefaultTable(tableName));
}
// do the actual deleting/truncating
if (useInMemoryDatabase())
DatabaseOperation.DELETE_ALL.execute(dbUnitConn, dataset);
// turn constraints back on for this connection
if (useInMemoryDatabase()) {
// for the hsql database
String sql = "SET REFERENTIAL_INTEGRITY TRUE";
PreparedStatement ps = connection.prepareStatement(sql);
ps.execute();
ps.close();
}
// clear the (hibernate) session to make sure nothing is cached, etc
Context.clearSession();
// needed because the authenticatedUser is the only object that sticks
// around after tests and the clearSession call
if (Context.isSessionOpen())
Context.refreshAuthenticatedUser();
}
/**
* Method to clear the hibernate cache
*/
@Before
public void clearHibernateCache() {
SessionFactory sf = (SessionFactory) applicationContext.getBean("sessionFactory");
// Map<String, ClassMetadata> classMetadata = sf.getAllClassMetadata();
// for (ClassMetadata cmd : classMetadata.values()) {
// EntityPersister ep = ((SessionFactoryImpl) sf).getEntityPersister(cmd.getEntityName());
// if (ep.hasCache()) {
// sf.evictEntity(ep.getCacheAccessStrategy().getRegion().getName());
// }
// }
//
// Map<String, CollectionMetadata> collMetadata = sf.getAllCollectionMetadata();
// for (CollectionMetadata cmd : collMetadata.values()) {
// CollectionPersister acp = ((SessionFactoryImpl) sf).getCollectionPersister(cmd.getRole());
// if (acp.hasCache()) {
// sf.evictCollection(acp.getCacheAccessStrategy().getRegion().getName());
// }
// }
sf.getCache().evictCollectionRegions();
sf.getCache().evictEntityRegions();
}
/**
* This method is run before all test methods that extend this {@link BaseContextSensitiveTest}
* unless you annotate your method with the "@SkipBaseSetup" annotation After running this
* method an in-memory database will be available that has the content of the rows from
* {@link #INITIAL_XML_DATASET_PACKAGE_PATH} and {@link #EXAMPLE_XML_DATASET_PACKAGE_PATH} xml
* files. This method will also ask to be authenticated against the current Context and
* database. The {@link #initializeInMemoryDatabase()} method has a user of admin:test.
*
* @see SkipBaseSetup
* @see SkipBaseSetupAnnotationExecutionListener
* @see #initializeInMemoryDatabase()
* @see #authenticate()
* @throws Exception
*/
@Before
public void baseSetupWithStandardDataAndAuthentication() throws Exception {
// only open one session per class
if (!Context.isSessionOpen()) {
Context.openSession();
}
// the skipBaseSetup flag is controlled by the @SkipBaseSetup
// annotation. If it is deflagged or if the developer has
// marked this class as a non-inmemory database, skip these base steps
if (skipBaseSetup == false && useInMemoryDatabase()) {
initializeInMemoryDatabase();
executeDataSet(EXAMPLE_XML_DATASET_PACKAGE_PATH);
authenticate();
}
Context.clearSession();
}
/**
* Called after each test class. This is called once per test class that extends
* {@link BaseContextSensitiveTest}. Needed so that "unit of work" that is the test class is
* surrounded by a pair of open/close session calls.
*
* @throws Exception
*/
@AfterClass
public static void closeSessionAfterEachClass() throws Exception {
// close any modules that might have been loaded by the @StartModules class annotation
ModuleUtil.shutdown();
// clean up the session so we don't leak memory
Context.closeSession();
}
/**
* Instance variable used by the {@link #baseSetupWithStandardDataAndAuthentication()} method to
* know whether the current "@Test" method has asked to be _not_ do the initialize/standard
* data/authenticate
*
* @see SkipBaseSetup
* @see SkipBaseSetupAnnotationExecutionListener
* @see #baseSetupWithStandardDataAndAuthentication()
*/
private boolean skipBaseSetup = false;
/**
* Don't run the {@link #setupDatabaseWithStandardData()} method. This means that the associated
* "@Test" must call one of these:
*
* <pre>
* * initializeInMemoryDatabase() ;
* * executeDataSet(EXAMPLE_DATA_SET);
* * Authenticate
* </pre>
*
* on its own if any of those results are needed. This method is called before all "@Test"
* methods that have been annotated with the "@SkipBaseSetup" annotation.
*
* @throws Exception
* @see SkipBaseSetup
* @see SkipBaseSetupAnnotationExecutionListener
* @see #baseSetupWithStandardDataAndAuthentication()
*/
public void skipBaseSetup() throws Exception {
skipBaseSetup = true;
}
}