/*************************GO-LICENSE-START*********************************
* Copyright 2014 ThoughtWorks, Inc.
*
* 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.
*************************GO-LICENSE-END***********************************/
package com.thoughtworks.go.server.database;
import java.io.File;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import javax.sql.DataSource;
import com.thoughtworks.go.database.Database;
import com.thoughtworks.go.database.QueryExtensions;
import com.thoughtworks.go.server.util.H2EventListener;
import com.thoughtworks.go.util.SystemEnvironment;
import org.apache.commons.dbcp.BasicDataSource;
import org.apache.log4j.Logger;
import org.h2.tools.Server;
import static com.thoughtworks.go.server.util.SqlUtil.joinWithQuotesForSql;
import static com.thoughtworks.go.util.ExceptionUtils.bomb;
/**
* @understands database administration
*/
public class H2Database implements Database {
private static final Logger LOG = Logger.getLogger(H2Database.class);
static final String DIALECT_H2 = "org.hibernate.dialect.H2Dialect";
private final H2Configuration configuration;
private final SystemEnvironment systemEnvironment;
private BasicDataSource dataSource;
private Server tcpServer;
public H2Database(SystemEnvironment systemEnvironment) {
this.systemEnvironment = systemEnvironment;
this.configuration = new H2Configuration(systemEnvironment);
}
@Override
public String dialectForHibernate() {
return DIALECT_H2;
}
@Override
public String getType() {
return "h2";
}
public void startDatabase() {
try {
new Migrate().execute(systemEnvironment.getDbPath(), true, configuration.getUser(),
configuration.getPassword(), false);
} catch (Exception e) {
throw new RuntimeException(e);
}
if (systemEnvironment.inDbDebugMode()) {
if (tcpServer != null) {
return;
}
try {
DataSource ds = createDataSource();
Connection con = ds.getConnection();
ResultSet set = con.getMetaData().getTables(null, null, null, null);
set.next();
set.close();
con.close();
LOG.info("Database is already running.");
return;
} catch (Exception e) {
LOG.info("Database is not running - starting a new one.");
}
try {
LOG.info("Starting h2 server in debug mode : "
+ "port=" + configuration.getPort()
+ " baseDir=" + systemEnvironment.getDbPath().getCanonicalPath());
String[] args = {
"-tcp",
"-tcpAllowOthers",
"-tcpPort", String.valueOf(configuration.getPort()),
"-baseDir", systemEnvironment.getDbPath().getCanonicalPath()
};
tcpServer = Server.createTcpServer(args);
tcpServer.start();
} catch (Exception e) {
bomb("Could not create database server.", e);
}
}
}
private BasicDataSource createDataSource(Boolean mvccEnabled) {
if (this.dataSource == null) {
BasicDataSource source = new BasicDataSource();
if (systemEnvironment.inDbDebugMode()) {
String url = String.format("jdbc:h2:tcp://%s:%s/%s", configuration.getHost(),
configuration.getPort(), configuration.getName());
configureDataSource(source, url);
LOG.info("Creating debug data source on port=" + configuration.getPort());
} else {
String url = dburl(mvccEnabled);
configureDataSource(source, url);
LOG.info("Creating data source with url=" + url);
}
this.dataSource = source;
}
return dataSource;
}
private void configureDataSource(BasicDataSource source, String url) {
String databaseUsername = configuration.getUser();
String databasePassword = configuration.getPassword();
LOG.info(String.format("[db] Using connection configuration %s [User: %s]", url, databaseUsername));
source.setDriverClassName("org.h2.Driver");
source.setUrl(url);
source.setUsername(databaseUsername);
source.setPassword(databasePassword);
source.setMaxActive(configuration.getMaxActive());
source.setMaxIdle(configuration.getMaxIdle());
}
public BasicDataSource createDataSource() {
return createDataSource(Boolean.TRUE);
}
private String dburl(Boolean mvccEnabled) {
return "jdbc:h2:" + systemEnvironment.getDbPath() + "/" + configuration.getName()
+ ";DB_CLOSE_DELAY=-1"
+ ";DB_CLOSE_ON_EXIT=FALSE"
+ ";MVCC=" + mvccEnabled.toString().toUpperCase()
+ ";CACHE_SIZE=" + systemEnvironment.getCruiseDbCacheSize()
+ ";TRACE_LEVEL_FILE=" + systemEnvironment.getCruiseDbTraceLevel()
+ ";TRACE_MAX_FILE_SIZE=" + systemEnvironment.getCruiseDbTraceFileSize()
// Commented out until H2 fix their bug
// + ";CACHE_TYPE=SOFT_LRU" //See http://www.h2database.com/html/changelog.html
+ ";DATABASE_EVENT_LISTENER='" + H2EventListener.class.getName() + "'";
}
public void upgrade() throws SQLException {
BasicDataSource source = createDataSource(Boolean.FALSE);
if (systemEnvironment.inDbDebugMode()) {
LOG.info("In debug mode - not upgrading database");
//don't upgrade
} else {
Migration upgradeToH2 = new MigrateHsqldbToH2(source, systemEnvironment);
upgradeToH2.migrate();
Migration migrateSchema = new DbDeployMigration(source, systemEnvironment);
migrateSchema.migrate();
}
shutdown();
}
public void shutdown() throws SQLException {
if (systemEnvironment.inDbDebugMode()) {
LOG.info("Shutting down database server.");
if (tcpServer == null) {
return;
}
if (dataSource != null) {
dataSource.close();
}
dataSource = null;
tcpServer.stop();
tcpServer = null;
} else {
Connection connection = createDataSource().getConnection();
Statement statement = connection.createStatement();
statement.execute("SHUTDOWN");
statement.close();
dataSource.close();
dataSource = null;
}
}
public void backup(File file) {
Connection connection = null;
try {
connection = createDataSource().getConnection();
Statement statement = connection.createStatement();
File dbBackupFile = new File(file, "db.zip");
statement.execute(String.format("BACKUP TO '%s'", dbBackupFile));
} catch (SQLException e) {
bomb(e);
} finally {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
// Ignore
}
}
}
}
@Override
public String getIbatisConfigXmlLocation() {
return null;
}
@Override
public QueryExtensions getQueryExtensions() {
return new QueryExtensions() {
@Override
public String queryFromInclusiveModificationsForPipelineRange(String pipelineName, Integer fromCounter, Integer toCounter) {
return "WITH LINK(id) AS ( "
+ " SELECT id "
+ " FROM pipelines "
+ " WHERE name = " + org.h2.util.StringUtils.quoteStringSQL(
pipelineName) // using string concatenation because Hibernate does not seem to be able to replace named or positional parameters here
+ " AND counter >= " + fromCounter
+ " AND counter <= " + toCounter
+ " UNION ALL "
+ " SELECT mod.pipelineId "
+ " FROM link "
+ " INNER JOIN pipelineMaterialRevisions pmr ON link.id = pmr.pipelineId "
+ " INNER JOIN modifications mod ON pmr.toRevisionId >= mod.id and pmr.actualFromRevisionId <= mod.id AND pmr.materialId = mod.materialId "
+ " WHERE mod.pipelineId IS NOT NULL"
+ ")"
+ "SELECT DISTINCT id FROM link WHERE id IS NOT NULL";
}
@Override
public String queryRelevantToLookedUpDependencyMap(List<Long> pipelineIds) {
return "WITH LINK(id, name, lookedUpId) AS ( "
+ " SELECT id, name, id as lookedUpId"
+ " FROM pipelines "
+ " WHERE id in (" + joinWithQuotesForSql(pipelineIds.toArray()) + ") "
+ " UNION ALL "
+ " SELECT mod.pipelineId as id, p.name as name, link.lookedUpId as lookedUpId "
+ " FROM link "
+ " INNER JOIN pipelineMaterialRevisions pmr ON link.id = pmr.pipelineId "
+ " INNER JOIN modifications mod ON pmr.toRevisionId >= mod.id and pmr.actualFromRevisionId <= mod.id AND pmr.materialId = mod.materialId "
+ " INNER JOIN pipelines p ON mod.pipelineId = p.id "
+ " WHERE mod.pipelineId IS NOT NULL"
+ ")"
+ "SELECT id, name, lookedUpId FROM link";
}
@Override
public String retrievePipelineTimeline() {
return "SELECT CAST(p.name AS VARCHAR), p.id AS p_id, p.counter, m.modifiedtime, "
+ " (SELECT CAST(materials.fingerprint AS VARCHAR) FROM materials WHERE id = m.materialId), naturalOrder, m.revision, pmr.folder, pmr.toRevisionId AS mod_id, pmr.Id as pmrid "
+ "FROM pipelines p, pipelinematerialrevisions pmr, modifications m "
+ "WHERE p.id = pmr.pipelineid "
+ "AND pmr.torevisionid = m.id "
+ "AND p.id > ?";
}
};
}
}