/*
* Copyright (c) 2011-2014 Jeppetto and Jonathan Thompson
*
* Licensed 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.iternine.jeppetto.testsupport.db;
import com.mongodb.MongoClient;
import com.mongodb.MongoException;
import org.dbunit.database.IDatabaseConnection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Properties;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public abstract class MongoDatabase extends Database {
//-------------------------------------------------------------
// Constants
//-------------------------------------------------------------
private static final Logger logger = LoggerFactory.getLogger(MongoDatabase.class);
//-------------------------------------------------------------
// Variables - Private - Static
//-------------------------------------------------------------
private static File mongod;
//-------------------------------------------------------------
// Methods - Public - Static
//-------------------------------------------------------------
public static MongoDatabase forPlatform(int port) {
String platform = System.getProperty("os.name", "unknown").toLowerCase();
if ("linux".equals(platform)) {
return new LinuxOrMacMongoDatabase(platform, port).initDatabase();
} else if ("mac os x".equals(platform)) {
return new LinuxOrMacMongoDatabase("mac", port).initDatabase();
} else {
throw new RuntimeException("Unknown platform for mongodb test context: " + platform);
}
}
//-------------------------------------------------------------
// Variables - Private
//-------------------------------------------------------------
private String mongoDbName;
private Process mongoProcess;
private final int mongoDbPort;
//-------------------------------------------------------------
// Constructor
//-------------------------------------------------------------
public MongoDatabase(int port) {
super(null);
this.mongoDbPort = port;
}
//-------------------------------------------------------------
// Methods - Public
//-------------------------------------------------------------
public void setMongoDbName(String mongoDbName) {
this.mongoDbName = mongoDbName;
}
//-------------------------------------------------------------
// Implementation - Database
//-------------------------------------------------------------
@Override
public void close() {
if (mongoDbName == null) {
return;
}
MongoClient mongoClient = null;
try {
mongoClient = new MongoClient("127.0.0.1", mongoDbPort);
mongoClient.dropDatabase(mongoDbName);
if (mongoClient.getDatabaseNames().contains(mongoDbName)) {
logger.error("Database {} will not go away!", mongoDbName);
}
} catch (UnknownHostException e) {
// weird
} catch (MongoException e) {
logger.warn("Could not drop database {}: {}", mongoDbName, e.getMessage());
} finally {
if (mongoClient != null) {
mongoClient.close();
}
}
}
@Override
protected void onNewIDatabaseConnection(IDatabaseConnection connection) {
throw new UnsupportedOperationException("MongoDatabase does not support new IDatabaseConnections.");
}
//-------------------------------------------------------------
// Methods - Protected - Abstract
//-------------------------------------------------------------
protected abstract String getPlatform();
protected abstract void makeExecutable(File file);
protected abstract ProcessBuilder createMongoProcess(File mongod, File dbpath, int port)
throws IOException;
//-------------------------------------------------------------
// Methods - Protected
//-------------------------------------------------------------
protected MongoDatabase initDatabase() {
if (alreadyRunning(mongoDbPort)) {
logger.debug("Mongo already running, using existing server.");
return this;
} else {
return downloadExtractAndStartMongo();
}
}
//-------------------------------------------------------------
// Methods - Private
//-------------------------------------------------------------
private int getMongoDbWebPort() {
return mongoDbPort + 1000;
}
private boolean alreadyRunning(int port) {
try {
Socket socket = new Socket("localhost", port);
socket.close();
return true;
} catch (IOException e) {
return false;
}
}
private MongoDatabase downloadExtractAndStartMongo() {
try {
File dbpath = createDataDir();
ProcessBuilder processBuilder = createMongoProcess(findMongod(), dbpath, mongoDbPort).redirectErrorStream(true);
mongoProcess = processBuilder.start();
// this thread will die when the process does
Thread readerDaemon = new Thread(new Runnable() {
@Override
public void run() {
try {
final BufferedReader reader = new BufferedReader(new InputStreamReader(mongoProcess.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
logger.debug(line);
}
} catch (IOException e) {
// bury
}
logger.debug("Mongod out consumer daemon exit.");
}
});
readerDaemon.setDaemon(true);
readerDaemon.start();
if (!waitForMongo(getMongoDbWebPort()) ) {
try {
throw new RuntimeException("Mongod process aborted with code " + mongoProcess.exitValue());
} catch (IllegalThreadStateException e) {
throw new RuntimeException("Mongod process still running but it doesn't seem to be running normally.");
}
}
logger.debug("Mongod seems to be running: http://localhost:{}", getMongoDbWebPort());
} catch (Exception e) {
this.close();
throw new RuntimeException(e);
}
return this;
}
private File findMongod()
throws IOException {
if (mongod != null) {
return mongod;
}
File mongoArchive = findMongoArchive();
if (!mongoArchive.exists()) {
throw new RuntimeException("Mongo archive not found at: " + mongoArchive.getAbsolutePath());
}
logger.debug(String.format("Extracting mongod from %s%n", mongoArchive.getAbsolutePath()));
ZipFile zip = new ZipFile(mongoArchive);
// TODO : this won't work on windows
ZipEntry mongodEntry = zip.getEntry("bin/mongod");
InputStream input = zip.getInputStream(mongodEntry);
mongod = new File(System.getProperty("java.io.tmpdir", "/tmp"), "mongod-bin/mongod");
if (!mongod.getParentFile().mkdirs()) {
logger.warn("Could not create parent dir for mongod, either a permission issue or dir was left behind by previous process.");
}
copy(input, mongod);
zip.close();
logger.debug(String.format("Copied mongod binary to %s%n", mongod.getAbsolutePath()));
return mongod;
}
private File findMongoArchive() {
String mavenLocalRepo = System.getProperty("maven.repo.local");
String actualRepo = (mavenLocalRepo == null
|| mavenLocalRepo.isEmpty()
|| mavenLocalRepo.contains("$")) ? System.getProperty("user.home") + "/.m2/repository"
: mavenLocalRepo;
return new File(String.format("%3$s/org/mongodb/mongod-binary/%1$s/mongod-binary-%1$s-%2$s.zip",
getMongoVersion(),
getPlatform(),
actualRepo));
}
private File createDataDir() {
File tmpDir = new File(System.getProperty("java.io.tmpdir", "/tmp"));
File datadir = new File(tmpDir, "mongodb-data");
if (!datadir.mkdirs()) {
logger.warn("Could not create data dir {}. Either a permissions issue, or the data dir was left behind by a previous process.",
datadir.getAbsolutePath());
}
return datadir;
}
private String getMongoVersion() {
Properties mongoProps = new Properties();
try {
mongoProps.load(getClass().getClassLoader().getResourceAsStream("mongo.properties"));
return mongoProps.getProperty("mongo.version");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private boolean waitForMongo(int port)
throws InterruptedException {
int secondsToWait = 7;
for (int i = 0; i < secondsToWait + 1; i++) {
if (alreadyRunning(port)) {
return true;
} else if (i < secondsToWait) {
Thread.sleep(1000L);
}
}
return false;
}
private void copy(InputStream input, File output)
throws IOException {
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(output));
byte[] buffer = new byte[10240];
int len;
while ((len = input.read(buffer)) >= 0) {
out.write(buffer, 0, len);
}
input.close();
out.close();
makeExecutable(output);
}
//-------------------------------------------------------------
// Inner Classes
//-------------------------------------------------------------
private static class LinuxOrMacMongoDatabase
extends MongoDatabase {
//-------------------------------------------------------------
// Variables - Private
//-------------------------------------------------------------
private String platform;
//-------------------------------------------------------------
// Constructor
//-------------------------------------------------------------
public LinuxOrMacMongoDatabase(String platform, int port) {
super(port);
this.platform = platform;
}
//-------------------------------------------------------------
// Implementation - MongoDatabase
//-------------------------------------------------------------
@Override
protected String getPlatform() {
return platform;
}
@Override
protected void makeExecutable(File file) {
ProcessBuilder pb = new ProcessBuilder("chmod", "+x", file.getAbsolutePath());
try {
Process p = pb.start();
int exit = p.waitFor();
if (exit != 0) {
throw new RuntimeException("chmod of " + file.getAbsolutePath() + " returned " + exit);
}
} catch (IOException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
protected ProcessBuilder createMongoProcess(File mongod, File dbpath, int port)
throws IOException {
return new ProcessBuilder(mongod.getAbsolutePath(),
"--dbpath", dbpath.getAbsolutePath(),
"--quiet",
"--bind_ip", "localhost",
"--smallfiles",
"--noprealloc",
"--port", Integer.toString(port));
}
}
}