org.geogig.geoserver.config.LogStoreInitializer.java Source code

Java tutorial

Introduction

Here is the source code for org.geogig.geoserver.config.LogStoreInitializer.java

Source

/* (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;
        }
    }
}