/* (c) 2016 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geogig.geoserver.config;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.geogig.geoserver.config.LogStore.PROP_DRIVER_CLASS;
import static org.geogig.geoserver.config.LogStore.PROP_ENABLED;
import static org.geogig.geoserver.config.LogStore.PROP_MAX_CONNECTIONS;
import static org.geogig.geoserver.config.LogStore.PROP_PASSWORD;
import static org.geogig.geoserver.config.LogStore.PROP_RUN_SCRIPT;
import static org.geogig.geoserver.config.LogStore.PROP_SCRIPT;
import static org.geogig.geoserver.config.LogStore.PROP_URL;
import static org.geogig.geoserver.config.LogStore.PROP_USER;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringReader;
import java.io.Writer;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.net.URL;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.logging.Logger;
import javax.sql.DataSource;
import org.geotools.util.logging.Logging;
import com.google.common.base.Charsets;
import com.google.common.base.Throwables;
import com.google.common.io.CharStreams;
import com.google.common.io.Resources;
import com.google.common.reflect.Reflection;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
/**
* Helper class to create the default {@code <data-dir>/geogig/config/security/logstore.properties}
* config file and the {@code <data-dir>/geogig/config/security/securitylogs.db} SQLite database
* where to write the log entries to.
* <p>
* It also copies a couple sql init scripts for oher database engines to the same directory.
*/
class LogStoreInitializer {
private static final Logger LOGGER = Logging.getLogger(LogStoreInitializer.class);
static void dispose(DataSource dataSource) {
if (dataSource instanceof HikariDataSource) {
((HikariDataSource) dataSource).close();
} else if (dataSource instanceof SingleConnectionDataSource) {
try {
((SingleConnectionDataSource) dataSource).conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
static DataSource newDataSource(final Properties properties, final File configFile) {
final String driverName = properties.getProperty(PROP_DRIVER_CLASS);
checkNotNull(driverName, "driverName not provided in properties file %s", configFile);
try {
Class.forName(driverName);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException(
String.format("JDBC Driver '%s' does not exist in the classpath", driverName));
}
final String jdbcUrl = properties.getProperty(PROP_URL);
checkArgument(jdbcUrl != null, "url not provided in properties file %s", configFile);
final String username = properties.getProperty(PROP_USER);
final String password = properties.getProperty(PROP_PASSWORD);
final String maxConnectionsProp = properties.getProperty(PROP_MAX_CONNECTIONS);
int maxConnections = 10;
if (maxConnectionsProp != null) {
try {
maxConnections = Integer.parseInt(maxConnectionsProp);
checkArgument(maxConnections > 0, "maxConnections must be an integer > 0: %s",
maxConnections);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
"Can't parse maxConnections as an int: " + maxConnectionsProp, e);
}
}
DataSource dataSource;
if (jdbcUrl.startsWith("jdbc:sqlite")) {
// sqlite must be used with a single connection shared among all threads
Connection connection;
try {
connection = DriverManager.getConnection(jdbcUrl);
} catch (SQLException e) {
throw Throwables.propagate(e);
}
dataSource = new SingleConnectionDataSource(connection);
} else {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(jdbcUrl);
config.setUsername(username);
config.setPassword(password);
config.setMaximumPoolSize(maxConnections);
dataSource = new HikariDataSource(config);
}
return dataSource;
}
static void createDefaultConfig(final File propertiesFile) throws IOException {
final File configDirectory = propertiesFile.getParentFile();
final File dbFile = new File(configDirectory, "securitylogs.db");
final String driverClassName = "org.sqlite.JDBC";
final String jdbcUrl = "jdbc:sqlite:" + dbFile.getAbsolutePath();
createDefaultPropertiesFile(propertiesFile, driverClassName, jdbcUrl);
}
private static void createDefaultPropertiesFile(final File propertiesFile,
final String driverClassName, final String jdbcUrl) {
Properties props = new Properties();
props.setProperty(PROP_ENABLED, "true");
props.setProperty(PROP_DRIVER_CLASS, driverClassName);
props.setProperty(PROP_URL, jdbcUrl);
props.setProperty(PROP_USER, "");
props.setProperty(PROP_PASSWORD, "");
props.setProperty(PROP_MAX_CONNECTIONS, "1");
props.setProperty(PROP_SCRIPT, "sqlite.sql");
props.setProperty(PROP_RUN_SCRIPT, "true");
try {
propertiesFile.createNewFile();
} catch (IOException e) {
throw Throwables.propagate(e);
}
saveConfig(props, propertiesFile);
}
static void saveConfig(Properties props, File propertiesFile) {
String comments = configComments();
try (Writer writer = new OutputStreamWriter(new FileOutputStream(propertiesFile),
Charsets.UTF_8)) {
props.store(writer, comments);
} catch (IOException e) {
throw Throwables.propagate(e);
}
}
private static String configComments() {
String comments = new StringBuilder(
"Connection information for the geogig security logs database.\n#")//
.append("enabled true|false whether to enable security logging\n#")
.append(PROP_DRIVER_CLASS)//
.append(": JDBC Driver class name\n#").append(PROP_URL)//
.append(": JDBC URL for the connections\n#").append(PROP_USER)//
.append(": database user name\n#")//
.append(PROP_PASSWORD)//
.append(": database user password\n#")//
.append(PROP_MAX_CONNECTIONS)//
.append(": max number of connections in the pool\n#")//
.append(PROP_SCRIPT)//
.append(": Database initialization DDL script file\n#")//
.append(PROP_RUN_SCRIPT)//
.append(": Boolean indicating whether to execute the init script. If true, and succeeded, its value will automatically be set to false afterwards\n#")//
.append("If using SQLite, the ")//
.append(PROP_MAX_CONNECTIONS)//
.append(" option has no effect and a single connection is used among all threads.\n")//
.append("If not using SQLite (for which the tables are created automatically), make sure to first run the\n#")//
.append("appropriate DDL script on the database. Some sample ones accompany this file. There are\n#")//
.append("more init scripts at https://github.com/qos-ch/logback/tree/master/logback-classic/src/main/resources/ch/qos/logback/classic/db/script")//
.toString();
return comments;
}
static void copySampleInitSript(File configDirectory, String scriptName) throws IOException {
File file = new File(configDirectory, scriptName);
if (file.exists()) {
return;
}
file.createNewFile();
try (OutputStream out = new FileOutputStream(file)) {
Resources.copy(LogStoreInitializer.class.getResource(scriptName), out);
}
}
static void runScript(DataSource ds, URL script) {
List<String> statements = parseStatements(script);
try {
try (Connection connection = ds.getConnection()) {
LOGGER.info("Running script " + script.getFile());
for (String sql : statements) {
try (Statement st = connection.createStatement()) {
LOGGER.fine(sql);
st.execute(sql);
} catch (SQLException e) {
throw Throwables.propagate(e);
}
}
}
} catch (SQLException e) {
throw Throwables.propagate(e);
}
}
private static List<String> parseStatements(URL script) {
List<String> lines;
try {
OutputStream to = new ByteArrayOutputStream();
Resources.copy(script, to);
String scriptContents = to.toString();
lines = CharStreams.readLines(new StringReader(scriptContents));
} catch (IOException e) {
throw Throwables.propagate(e);
}
List<String> statements = new ArrayList<String>();
StringBuilder sb = new StringBuilder();
for (String line : lines) {
line = line.trim();
if (line.startsWith("#") || line.startsWith("-") || line.isEmpty()) {
continue;
}
sb.append(line).append('\n');
if (line.endsWith(";")) {
statements.add(sb.toString());
sb.setLength(0);
}
}
return statements;
}
private static class SingleConnectionDataSource implements DataSource {
private Connection conn;
public SingleConnectionDataSource(Connection conn) {
this.conn = Unclosable.proxyFor(conn);
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
@Override
public Connection getConnection() throws SQLException {
return conn;
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return conn;
}
}
private static class Unclosable implements InvocationHandler {
private Connection c;
public Unclosable(Connection c) {
this.c = c;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("close".equals(method.getName())) {
return null;
}
return method.invoke(c, args);
}
public static Connection proxyFor(Connection c) {
Connection proxy = Reflection.newProxy(Connection.class, new Unclosable(c));
return proxy;
}
}
}