/*******************************************************************************
* This file is part of OpenNMS(R).
*
* Copyright (C) 2007-2011 The OpenNMS Group, Inc.
* OpenNMS(R) is Copyright (C) 1999-2011 The OpenNMS Group, Inc.
*
* OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
*
* OpenNMS(R) is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published
* by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* OpenNMS(R) is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenNMS(R). If not, see:
* http://www.gnu.org/licenses/
*
* For more information contact:
* OpenNMS(R) Licensing <license@opennms.org>
* http://www.opennms.org/
* http://www.opennms.com/
*******************************************************************************/
package org.opennms.netmgt.dao.db;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Comparator;
import java.util.Date;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import javax.sql.DataSource;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.opennms.test.ConfigurationTestUtils;
import org.springframework.jdbc.core.RowCountCallbackHandler;
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
import org.springframework.util.StringUtils;
/**
*
* @author <a href="mailto:brozow@opennms.org">Mathew Brozowski</a>
*/
public class TemporaryDatabase implements DataSource {
private static final String TEST_DB_NAME_PREFIX = "opennms_test_";
public static final String DRIVER_PROPERTY = "mock.db.driver";
public static final String URL_PROPERTY = "mock.db.url";
public static final String ADMIN_USER_PROPERTY = "mock.db.adminUser";
public static final String ADMIN_PASSWORD_PROPERTY = "mock.db.adminPassword";
public static final String DEFAULT_DRIVER = "org.postgresql.Driver";
public static final String DEFAULT_URL = "jdbc:postgresql://localhost:5432/";
public static final String DEFAULT_ADMIN_USER = "postgres";
public static final String DEFAULT_ADMIN_PASSWORD = "";
private static final int MAX_DATABASE_DROP_ATTEMPTS = 10;
private static final Object TEMPLATE1_MUTEX = new Object();
private final String m_testDatabase;
private final String m_driver;
private final String m_url;
private final String m_adminUser;
private final String m_adminPassword;
private final boolean m_useExisting;
private DataSource m_dataSource;
private DataSource m_adminDataSource;
private InstallerDb m_installerDb;
private ByteArrayOutputStream m_outputStream;
private boolean m_setupIpLike = true;
private boolean m_populateSchema = false;
private boolean m_destroyed = false;
private SimpleJdbcTemplate m_jdbcTemplate;
public TemporaryDatabase() throws Exception {
this(TEST_DB_NAME_PREFIX + System.currentTimeMillis());
}
public TemporaryDatabase(String testDatabase) throws Exception {
this(testDatabase, false);
}
public TemporaryDatabase(String testDatabase, boolean useExisting) throws Exception {
this(testDatabase, System.getProperty(DRIVER_PROPERTY, DEFAULT_DRIVER),
System.getProperty(URL_PROPERTY, DEFAULT_URL),
System.getProperty(ADMIN_USER_PROPERTY, DEFAULT_ADMIN_USER),
System.getProperty(ADMIN_PASSWORD_PROPERTY, DEFAULT_ADMIN_PASSWORD), useExisting);
}
public TemporaryDatabase(String testDatabase, String driver, String url,
String adminUser, String adminPassword) throws Exception {
this(testDatabase, driver, url, adminUser, adminPassword, false);
}
public TemporaryDatabase(String testDatabase, String driver, String url,
String adminUser, String adminPassword, boolean useExisting) throws Exception {
// Append the current object's hashcode to make this value truly unique
m_testDatabase = testDatabase;
m_driver = driver;
m_url = url;
m_adminUser = adminUser;
m_adminPassword = adminPassword;
m_useExisting = useExisting;
}
public void setPopulateSchema(boolean populateSchema) {
m_populateSchema = populateSchema;
}
public void create() throws Exception {
setupDatabase();
if (m_populateSchema) {
initializeDatabase();
}
}
private void initializeDatabase() throws Exception {
m_installerDb = new InstallerDb();
try {
// Create a ByteArrayOutputSteam to effectively throw away output.
resetOutputStream();
m_installerDb.setDatabaseName(getTestDatabase());
m_installerDb.setDataSource(getDataSource());
m_installerDb.setAdminDataSource(getAdminDataSource());
m_installerDb.setPostgresOpennmsUser(m_adminUser);
m_installerDb.setCreateSqlLocation(getCreateSqlLocation());
m_installerDb.setStoredProcedureDirectory(getStoredProcDirectory());
// installerDb.setDebug(true);
m_installerDb.readTables();
m_installerDb.createSequences();
m_installerDb.updatePlPgsql();
m_installerDb.addStoredProcedures();
if (isSetupIpLike()) {
if (!m_installerDb.isIpLikeUsable()) {
m_installerDb.setupPlPgsqlIplike();
}
}
m_installerDb.createTables();
m_installerDb.insertData();
} finally {
m_installerDb.closeConnection();
}
}
protected String getStoredProcDirectory() {
return ConfigurationTestUtils.getFileForConfigFile("create.sql").getParentFile().getAbsolutePath();
}
protected String getCreateSqlLocation() {
return ConfigurationTestUtils.getFileForConfigFile("create.sql").getAbsolutePath();
}
public boolean isSetupIpLike() {
return m_setupIpLike;
}
public void setSetupIpLike(boolean setupIpLike) {
m_setupIpLike = setupIpLike;
}
protected File findIpLikeLibrary() {
File topDir = ConfigurationTestUtils.getTopProjectDirectory();
File ipLikeDir = new File(topDir, "opennms-iplike");
assertTrue("iplike directory exists at ../opennms-iplike: " + ipLikeDir.getAbsolutePath(), ipLikeDir.exists());
File[] ipLikePlatformDirs = ipLikeDir.listFiles(new FileFilter() {
public boolean accept(File file) {
if (file.getName().matches("opennms-iplike-.*") && file.isDirectory()) {
return true;
} else {
return false;
}
}
});
assertTrue("expecting at least one opennms iplike platform directory in " + ipLikeDir.getAbsolutePath() + "; got: " + StringUtils.arrayToDelimitedString(ipLikePlatformDirs, ", "), ipLikePlatformDirs.length > 0);
File ipLikeFile = null;
for (File ipLikePlatformDir : ipLikePlatformDirs) {
assertTrue("iplike platform directory does not exist but was listed in directory listing: " + ipLikePlatformDir.getAbsolutePath(), ipLikePlatformDir.exists());
File ipLikeTargetDir = new File(ipLikePlatformDir, "target");
if (!ipLikeTargetDir.exists() || !ipLikeTargetDir.isDirectory()) {
// Skip this one
continue;
}
File[] ipLikeFiles = ipLikeTargetDir.listFiles(new FileFilter() {
public boolean accept(File file) {
if (file.isFile() && file.getName().matches("opennms-iplike-.*\\.(so|dylib)")) {
return true;
} else {
return false;
}
}
});
assertFalse("expecting zero or one iplike file in " + ipLikeTargetDir.getAbsolutePath() + "; got: " + StringUtils.arrayToDelimitedString(ipLikeFiles, ", "), ipLikeFiles.length > 1);
if (ipLikeFiles.length == 1) {
ipLikeFile = ipLikeFiles[0];
}
}
assertNotNull("Could not find iplike shared object in a target directory in any of these directories: " + StringUtils.arrayToDelimitedString(ipLikePlatformDirs, ", "), ipLikeFile);
return ipLikeFile;
}
private void assertNotNull(String string, Object o) {
if (o == null) {
throw new IllegalStateException(string);
}
}
private void assertFalse(String string, boolean b) {
if (b) {
throw new IllegalStateException(string);
}
}
private void assertTrue(String string, boolean b) {
if (!b) {
throw new IllegalStateException(string);
}
}
private void resetOutputStream() {
m_outputStream = new ByteArrayOutputStream();
m_installerDb.setOutputStream(new PrintStream(m_outputStream));
}
public void setupDatabase() throws Exception {
setDataSource(new SimpleDataSource(m_driver, m_url + getTestDatabase(),
m_adminUser, m_adminPassword));
setAdminDataSource(new SimpleDataSource(m_driver, m_url + "template1",
m_adminUser, m_adminPassword));
if (!m_useExisting) {
// Synchronize around a static mutex to prevent multiple connections
// to the template1 database
synchronized(TEMPLATE1_MUTEX) {
createTestDatabase();
}
}
// Test connecting to test database.
Connection connection = getConnection();
connection.close();
setJdbcTemplate(new SimpleJdbcTemplate(this));
}
private void createTestDatabase() throws Exception {
Connection adminConnection = getAdminDataSource().getConnection();
Statement st = null;
try {
st = adminConnection.createStatement();
st.execute("CREATE DATABASE " + getTestDatabase()
+ " WITH ENCODING='UNICODE'");
} finally {
if (st != null) {
st.close();
}
adminConnection.close();
}
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
Thread.sleep(100);
destroyTestDatabase();
} catch (Throwable e) {
e.printStackTrace();
}
}
});
}
public void drop() throws Exception {
if (!m_useExisting) {
destroyTestDatabase();
}
}
private void destroyTestDatabase() throws Exception {
if (m_useExisting) {
return;
}
if (m_destroyed) {
System.err.println("Database '" + getTestDatabase() + "' already destroyed");
// database already destroyed
return;
}
/*
* Sleep before destroying the test database because PostgreSQL doesn't
* seem to notice immediately clients have disconnected. Yeah, it's a
* hack.
*/
Thread.sleep(100);
Connection adminConnection = getAdminDataSource().getConnection();
try {
for (int dropAttempt = 0; dropAttempt < MAX_DATABASE_DROP_ATTEMPTS; dropAttempt++) {
Statement st = null;
try {
st = adminConnection.createStatement();
st.execute("DROP DATABASE " + getTestDatabase());
break;
} catch (SQLException e) {
if ((dropAttempt + 1) >= MAX_DATABASE_DROP_ATTEMPTS) {
final String message = "Failed to drop test database on last attempt " + (dropAttempt + 1) + ": " + e;
System.err.println(new Date().toString() + ": " + message);
dumpThreads();
SQLException newException = new SQLException(message);
newException.initCause(e);
throw newException;
} else {
System.err.println(new Date().toString() + ": Failed to drop test database on attempt " + (dropAttempt + 1) + ": " + e);
Thread.sleep(1000);
}
} finally {
if (st != null) {
st.close();
st = null;
}
}
}
} finally {
/*
* Since we are already going to be throwing an exception at this
* point, print any further errors to stdout so we don't mask the
* first failure.
*/
try {
adminConnection.close();
} catch (SQLException e) {
System.err.println("Error closing administrative database "
+ "connection after attempting to drop "
+ "test database");
e.printStackTrace();
}
/*
* Sleep after disconnecting from template1, otherwise creating a
* new test database in future tests may fail. Man, I hate this.
*/
Thread.sleep(100);
}
m_destroyed = true;
}
public static void dumpThreads() {
Map<Thread, StackTraceElement[]> threads = Thread.getAllStackTraces();
int daemons = 0;
for (Thread t : threads.keySet()) {
if (t.isDaemon()) {
daemons++;
}
}
System.err.println("Thread dump of " + threads.size() + " threads (" + daemons + " daemons):");
Map<Thread, StackTraceElement[]> sortedThreads = new TreeMap<Thread, StackTraceElement[]>(new Comparator<Thread>() {
public int compare(final Thread t1, final Thread t2) {
return Long.valueOf(t1.getId()).compareTo(Long.valueOf(t2.getId()));
}
});
sortedThreads.putAll(threads);
for (Entry<Thread, StackTraceElement[]> entry : sortedThreads.entrySet()) {
Thread thread = entry.getKey();
System.err.println("Thread " + thread.getId() + (thread.isDaemon() ? " (daemon)" : "") + ": " + thread + " (state: " + thread.getState() + ")");
for (StackTraceElement e : entry.getValue()) {
System.err.println("\t" + e);
}
}
System.err.println("Thread dump completed.");
}
@Override
public Connection getConnection() throws SQLException {
return m_dataSource.getConnection();
}
public void update(String stmt, Object... values) {
// StringBuffer buf = new StringBuffer("[");
// for(int i = 0; i < values.length; i++) {
// if (i != 0)
// buf.append(", ");
// buf.append(values[i]);
// }
// buf.append("]");
// MockUtil.println("Executing "+stmt+" with values "+buf);
getJdbcTemplate().update(stmt, values);
}
public int countRows(String sql, Object... values) {
RowCountCallbackHandler counter = new RowCountCallbackHandler();
getJdbcTemplate().getJdbcOperations().query(sql, values, counter);
return counter.getRowCount();
}
public String getNextSequenceValStatement(String seqName) {
return "select nextval('" + seqName + "')";
}
protected Integer getNextId(String nxtIdStmt) {
return getJdbcTemplate().queryForInt(nxtIdStmt);
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return m_dataSource.getConnection(username, password);
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return m_dataSource.getLogWriter();
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
m_dataSource.setLogWriter(out);
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
m_dataSource.setLoginTimeout(seconds);
}
@Override
public int getLoginTimeout() throws SQLException {
return m_dataSource.getLoginTimeout();
}
public SimpleJdbcTemplate getJdbcTemplate() {
return m_jdbcTemplate;
}
public void setJdbcTemplate(SimpleJdbcTemplate jdbcTemplate) {
m_jdbcTemplate = jdbcTemplate;
}
public DataSource getAdminDataSource() {
return m_adminDataSource;
}
public void setAdminDataSource(DataSource adminDataSource) {
m_adminDataSource = adminDataSource;
}
public DataSource getDataSource() {
return m_dataSource;
}
public void setDataSource(DataSource dataSource) {
m_dataSource = dataSource;
}
public String getTestDatabase() {
return m_testDatabase;
}
/**
* Returns an object that implements the given interface to allow access to
* non-standard methods, or standard methods not exposed by the proxy.
* <p/>
* If the receiver implements the interface then the result is the receiver
* or a proxy for the receiver. If the receiver is a wrapper
* and the wrapped object implements the interface then the result is the
* wrapped object or a proxy for the wrapped object. Otherwise return the
* the result of calling <code>unwrap</code> recursively on the wrapped object
* or a proxy for that result. If the receiver is not a
* wrapper and does not implement the interface, then an <code>SQLException</code> is thrown.
*
* @param iface A Class defining an interface that the result must implement.
* @return an object that implements the interface. May be a proxy for the actual implementing object.
* @throws java.sql.SQLException If no object found that implements the interface
* @since 1.6
*/
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null; //TODO
}
/**
* Returns true if this either implements the interface argument or is directly or indirectly a wrapper
* for an object that does. Returns false otherwise. If this implements the interface then return true,
* else if this is a wrapper then return the result of recursively calling <code>isWrapperFor</code> on the wrapped
* object. If this does not implement the interface and is not a wrapper, return false.
* This method should be implemented as a low-cost operation compared to <code>unwrap</code> so that
* callers can use this method to avoid expensive <code>unwrap</code> calls that may fail. If this method
* returns true then calling <code>unwrap</code> with the same argument should succeed.
*
* @param iface a Class defining an interface.
* @return true if this implements the interface or directly or indirectly wraps an object that does.
* @throws java.sql.SQLException if an error occurs while determining whether this is a wrapper
* for an object with the given interface.
* @since 1.6
*/
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false; //TODO
}
public String getDriver() {
return m_driver;
}
public String getUrl() {
return m_url;
}
public String toString() {
return new ToStringBuilder(this)
.append("driver", m_driver)
.append("url", m_url)
.append("testDatabase", m_testDatabase)
.append("useExisting", m_useExisting)
.append("setupIpLike", m_setupIpLike)
.append("populateSchema", m_populateSchema)
.append("dataSource", m_dataSource)
.append("adminDataSource", m_adminDataSource)
.append("adminUser", m_adminUser)
.toString();
}
}