/*
* Copyright 2010-2013 Ning, Inc.
* Copyright 2014-2015 Groupon, Inc
* Copyright 2014-2015 The Billing Project, LLC
*
* The Billing Project licenses this file to you under the Apache License, version 2.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://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.killbill.commons.embeddeddb.mysql;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.sql.DataSource;
import org.killbill.commons.embeddeddb.EmbeddedDB;
import org.mariadb.jdbc.MariaDbDataSource;
import org.slf4j.event.Level;
import com.mysql.management.HackedMysqldResource;
import com.mysql.management.MysqldResourceI;
public class MySQLEmbeddedDB extends EmbeddedDB {
protected final AtomicBoolean started = new AtomicBoolean(false);
protected DataSource dataSource;
protected final int port;
private File dbDir;
private File dataDir;
private HackedMysqldResource mysqldResource;
public MySQLEmbeddedDB() {
// Avoid dashes - MySQL doesn't like them
this("database" + UUID.randomUUID().toString().substring(0, 8),
"user" + UUID.randomUUID().toString().substring(0, 8),
"pass" + UUID.randomUUID().toString().substring(0, 8));
}
public MySQLEmbeddedDB(final String databaseName, final String username, final String password) {
super(databaseName, username, password, null);
this.port = getPort();
this.jdbcConnectionString = "jdbc:mysql://localhost:" + port + "/" + databaseName + "?createDatabaseIfNotExist=true&allowMultiQueries=true";
}
@Override
public DBEngine getDBEngine() {
return DBEngine.MYSQL;
}
@Override
public void initialize() throws IOException {
}
@Override
public void start() throws IOException {
if (started.get()) {
throw new IOException("MySQL is already running: " + jdbcConnectionString);
}
startMysql();
createDataSource();
refreshTableNames();
}
@Override
public void refreshTableNames() throws IOException {
final String query = String.format("select table_name from information_schema.tables where table_schema = '%s' and table_type = 'BASE TABLE';", databaseName);
try {
executeQuery(query, new ResultSetJob() {
@Override
public void work(final ResultSet resultSet) throws SQLException {
allTables.clear();
while (resultSet.next()) {
allTables.add(resultSet.getString(1));
}
}
});
} catch (final SQLException e) {
throw new IOException(e);
}
}
@Override
public DataSource getDataSource() throws IOException {
if (!started.get()) {
throw new IOException("MySQL is not running");
}
return dataSource;
}
@Override
public void stop() throws IOException {
if (!started.get()) {
throw new IOException("MySQL is not running");
}
super.stop();
stopMysql();
}
@Override
public String getCmdLineConnectionString() {
return String.format("mysql -u%s -p%s -P%s -S%s/mysql.sock %s", username, password, port, dataDir, databaseName);
}
protected void createDataSource() throws IOException {
if (useConnectionPooling()) {
dataSource = createHikariDataSource();
} else {
final MariaDbDataSource mariaDBDataSource = new MariaDbDataSource();
try {
mariaDBDataSource.setUrl(jdbcConnectionString);
} catch (final SQLException e) {
throw new IOException(e);
}
mariaDBDataSource.setDatabaseName(databaseName);
mariaDBDataSource.setUser(username);
mariaDBDataSource.setPassword(password);
mariaDBDataSource.setPort(port);
dataSource = mariaDBDataSource;
}
}
private void startMysql() throws IOException {
dbDir = File.createTempFile("mysqldb", "");
if (!dbDir.delete()) {
logger.warn("Unable to delete " + dbDir.getAbsolutePath());
}
if (!dbDir.mkdir()) {
throw new IOException("Unable to create " + dbDir.getAbsolutePath());
}
dataDir = File.createTempFile("mysqldata", "");
if (!dataDir.delete()) {
logger.warn("Unable to delete " + dataDir.getAbsolutePath());
}
if (!dataDir.mkdir()) {
throw new IOException("Unable to create " + dataDir.getAbsolutePath());
}
final PrintStream out = new PrintStream(new LoggingOutputStream(logger, Level.INFO), true);
final PrintStream err = new PrintStream(new LoggingOutputStream(logger, Level.WARN), true);
final PrintStream debug = new PrintStream(new LoggingOutputStream(logger, Level.DEBUG), true);
mysqldResource = new HackedMysqldResource(dbDir, dataDir, null, out, err, debug);
final Map<String, String> dbOpts = new HashMap<String, String>();
dbOpts.put(MysqldResourceI.PORT, Integer.toString(port));
dbOpts.put(MysqldResourceI.INITIALIZE_USER, "true");
dbOpts.put(MysqldResourceI.INITIALIZE_PASSWORD, password);
dbOpts.put(MysqldResourceI.INITIALIZE_USER_NAME, username);
dbOpts.put("default-time-zone", "+00:00");
mysqldResource.start("test-mysqld-thread", dbOpts);
if (!mysqldResource.isRunning()) {
throw new IllegalStateException("MySQL did not start.");
} else {
started.set(true);
logger.info("MySQL started: " + getCmdLineConnectionString());
}
}
private void stopMysql() throws IOException {
if (mysqldResource != null) {
try {
mysqldResource.shutdown();
} catch (final NullPointerException npe) {
logger.warn("Failed to shutdown mysql properly ", npe);
}
try {
deleteRecursive(dataDir);
deleteRecursive(dbDir);
} catch (final FileNotFoundException e) {
throw new IOException(e);
}
started.set(false);
logger.info("MySQL stopped: " + getCmdLineConnectionString());
}
}
private static boolean deleteRecursive(final File path) throws FileNotFoundException {
if (!path.exists()) {
throw new FileNotFoundException(path.getAbsolutePath());
}
boolean ret = true;
if (path.isDirectory()) {
final File[] files = path.listFiles();
if (files != null) {
for (final File f : files) {
ret = ret && deleteRecursive(f);
}
}
}
return ret && path.delete();
}
}