All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.d3x.core.db.DatabaseConfig Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2018-2019 D3X Systems - All Rights Reserved
 *
 * 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 com.d3x.core.db;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.reflect.Type;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;

import com.d3x.core.json.Json;
import com.d3x.core.json.JsonEngine;
import com.d3x.core.json.JsonSchema;
import com.d3x.core.util.Crypto;
import com.d3x.core.util.IO;
import com.d3x.core.util.Option;
import com.d3x.core.util.Secret;
import com.google.gson.Gson;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;

/**
 * A class that capture database configuration details
 *
 * @author Xavier Witdouck
 */
@lombok.ToString()
@lombok.EqualsAndHashCode()
@lombok.AllArgsConstructor()
@lombok.Builder(toBuilder=true)
@lombok.extern.slf4j.Slf4j()
public class DatabaseConfig {

    public static final JsonSchema schema = JsonSchema.of(DatabaseConfig.class, "db-config", 1);

    /** The driver definition */
    @lombok.Getter @lombok.NonNull
    private DatabaseDriver driver;
    /** The JDBC connection url */
    @lombok.Getter @lombok.NonNull
    private String url;
    /** The JDBC user name*/
    @lombok.Getter @lombok.NonNull
    private Option user;
    /** The JDBC password */
    @lombok.Getter @lombok.NonNull
    private Option password;
    /** The initial size of the connection pool */
    @lombok.Getter @lombok.NonNull
    private Option initialPoolSize;
    /** The max size of connection pool */
    @lombok.Getter @lombok.NonNull
    private Option maxPoolSize;
    /** The max number of idle connections in pool */
    @lombok.Getter @lombok.NonNull
    private Option maxPoolIdleSize;
    /** True if connections should be set to read only */
    @lombok.Getter @lombok.NonNull
    private Option readOnly;
    /** True to mark connection as auto commit */
    @lombok.Getter @lombok.NonNull
    private Option autoCommit;
    /** The query time out in seconds */
    @lombok.Getter @lombok.NonNull
    private Option queryTimeOutSeconds;
    /** The max time to wait for an available connection */
    @lombok.Getter @lombok.NonNull
    private Option maxWaitTimeMillis;
    /** The default fetch size for statements */
    @lombok.Getter @lombok.NonNull
    private Option fetchSize;
    /** The connection properties */
    @lombok.NonNull
    private Map properties;


    /**
     * Returns the map of connection properties
     * @return      the map of connection properties
     */
    public Map getProperties() {
        return Collections.unmodifiableMap(properties);
    }


    /**
     * Returns a new database config for an H2 database
     * @param dbFile        the database file
     * @param user          the username
     * @param password      the password
     * @return              the config
     */
    public static DatabaseConfig h2(File dbFile, String user, String password) {
        return DatabaseConfig.of(config -> {
            config.driver(DatabaseDriver.H2);
            config.url("jdbc:h2:file:" + dbFile.getAbsolutePath());
            config.user(Option.of(user));
            config.password(Option.of(Secret.of(password)));
            config.properties(new HashMap<>());
        });
    }

    /**
     * Returns a new config for MySQL with the details provided
     * @param url           the jdbc url
     * @param username      the jdbc username
     * @param password      the jdbc password
     * @return              the newly created config
     */
    public static DatabaseConfig mysql(String url, String username, Secret password) {
        return DatabaseConfig.of(config -> {
            config.driver(DatabaseDriver.MYSQL);
            config.url(url);
            config.user(Option.of(username));
            config.password(Option.of(password));
            config.properties(new HashMap<>());
        });
    }

    /**
     * Returns a new database config for the args provided
     * @param driver        the database driver
     * @param url           the database url
     * @param user          the username
     * @param password      the password
     * @return              the config
     */
    public static DatabaseConfig of(DatabaseDriver driver, String url, String user, Secret password) {
        return DatabaseConfig.of(config -> {
            config.driver(driver);
            config.url(url);
            config.user(Option.of(user));
            config.password(Option.of(password));
            config.properties(new HashMap<>());
        });
    }


    /**
     * Returns database config loaded from a JSON resource
     * @param url       the URL to load config from
     * @param crypto    the optional crypto if password is encrypted
     * @return          the database config
     */
    public static DatabaseConfig fromJson(URL url, Option crypto) throws IOException {
        return gson(crypto).fromJson(new InputStreamReader(url.openStream()), DatabaseConfig.class);
    }


    /**
     * Returns the config loaded from a json file on classpath
     * @param resource      the path to json resource
     * @return              the database config
     * @throws DatabaseException    if fails to initialize config
     */
    public static DatabaseConfig fromJson(String resource) throws DatabaseException {
        try {
            var url = DatabaseConfig.class.getResource(resource);
            if (url == null) {
                throw new RuntimeException("No config resource for path: " + resource);
            } else {
                var home = new File(System.getProperty("user.home"));
                var keyPath = new File(home, ".d3x/keys/db.key").getAbsolutePath();
                var keyFile = new File(System.getProperty("db.key", keyPath));
                if (!keyFile.exists()) {
                    throw new RuntimeException("Missing cryptographic secret key at: " + keyFile.getAbsolutePath());
                } else {
                    var keyUrl = keyFile.toURI().toURL();
                    var crypto = Crypto.withSecretKey("AES", keyUrl);
                    log.info("Loading database config from: " + resource);
                    return DatabaseConfig.fromJson(url, Option.of(crypto));
                }
            }
        } catch (Exception ex) {
            throw new DatabaseException("Failed to initialise database from config: " + resource, ex);
        }
    }


    /**
     * Writes this config as JSON to the output stream provided
     * @param crypto    the optional crypto for password encryption
     * @param os        the output stream to write to
     */
    public void toJson(Option crypto, OutputStream os) throws IOException {
        OutputStreamWriter writer = null;
        try {
            writer = new OutputStreamWriter(new BufferedOutputStream(os), StandardCharsets.UTF_8);
            DatabaseConfig.gson(crypto).toJson(this, writer);
        } finally {
            if (writer != null) {
                writer.flush();
                writer.close();
            }
        }
    }


    /**
     * Returns a new database config base on the consumer actions
     * @param consumer  the consumer to configure builder
     * @return          the newly created config
     */
    public static DatabaseConfig of(Consumer consumer) {
        var builder = DatabaseConfig.builder();
        builder.readOnly(Option.of(false));
        builder.autoCommit(Option.of(true));
        builder.queryTimeOutSeconds(Option.of(60));
        builder.initialPoolSize(Option.of(5));
        builder.maxPoolSize(Option.of(10));
        builder.maxWaitTimeMillis(Option.of(5000));
        builder.maxPoolIdleSize(Option.of(10));
        builder.fetchSize(Option.empty());
        consumer.accept(builder);
        return builder.build();
    }


    /**
     * Returns a GSON adapter that can serialise DatabaseConfig
     * @param crypto    the optional crypto for password encryption
     * @return          the GSON instance
     */
    public static Gson gson(Option crypto) {
        var engine = new JsonEngine();
        engine.register(schema, new Serializer());
        engine.register(schema, new Deserializer());
        if (crypto.isEmpty()) {
            return engine.getGson(1);
        } else {
            engine.crypto(crypto.get());
            return engine.getGson(1);
        }
    }


    /**
     * Attempts to connect to the database given this config to assess if it is valid
     * @return  this configuration object
     * @throws DatabaseException    if fails to make a connection
     */
    public DatabaseConfig verify() {
        Connection conn = null;
        final String className = driver.getDriverClassName();
        try {
            log.info("Connecting to " + url);
            Class.forName(className);
            final String pass = password.map(p -> new String(p.getValue())).orNull();
            conn = DriverManager.getConnection(url, user.orNull(), pass);
            log.info("Connection successful to " + url);
            return this;
        } catch (ClassNotFoundException ex) {
            throw new DatabaseException("Failed to load JDBC driver class " + className, ex);
        } catch (SQLException ex) {
            throw new DatabaseException("Failed to connect to database with " + url, ex);
        } finally {
            IO.close(conn);
        }
    }


    public static class Serializer implements JsonSerializer {
        @Override
        public JsonElement serialize(DatabaseConfig record, Type type, JsonSerializationContext context) {
            if (record == null) {
                return JsonNull.INSTANCE;
            } else {
                return Json.object(object -> {
                    object.addProperty("driver", record.getDriver().getDriverClassName());
                    object.addProperty("url", record.getUrl());
                    object.addProperty("user", record.getUser().orNull());
                    object.add("password", context.serialize(record.getPassword().orNull(), Secret.class));
                    object.addProperty("initialPoolSize", record.initialPoolSize.orNull());
                    object.addProperty("maxPoolSize", record.maxPoolSize.orNull());
                    object.addProperty("maxPoolIdleSize", record.maxPoolIdleSize.orNull());
                    object.addProperty("readOnly", record.readOnly.orNull());
                    object.addProperty("autoCommit", record.autoCommit.orNull());
                    object.addProperty("queryTimeOutSeconds", record.queryTimeOutSeconds.orNull());
                    object.addProperty("maxWaitTimeMills", record.maxWaitTimeMillis.orNull());
                    object.addProperty("fetchSize", record.fetchSize.orNull());
                    object.add("properties", Json.object(props -> {
                        var keys = record.properties.keySet().stream().sorted();
                        keys.forEach(key -> {
                            var value = record.properties.get(key);
                            if (value != null) {
                                props.addProperty(key, value);
                            }
                        });
                    }));
                });
            }
        }
    }


    public static class Deserializer implements JsonDeserializer {
        @Override
        public DatabaseConfig deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException {
            if (json == null || json == JsonNull.INSTANCE) return null;
            var object = json.getAsJsonObject();
            var properties = new HashMap();
            var propObject = Json.getObject(object, "properties").orElse(new JsonObject());
            propObject.entrySet().forEach(entry -> {
                var key = entry.getKey();
                var value = entry.getValue().getAsString();
                if (value != null) {
                    properties.put(key, value);
                }
            });
            return DatabaseConfig.builder()
                .driver(DatabaseDriver.of(Json.getStringOrFail(object, "driver")))
                .url(Json.getStringOrFail(object, "url"))
                .user(Json.getString(object, "user"))
                .password(Json.getElement(object, "password").map(e -> context.deserialize(e, Secret.class)))
                .initialPoolSize(Json.getInt(object, "initialPoolSize"))
                .maxPoolSize(Json.getInt(object, "maxPoolSize"))
                .maxPoolIdleSize(Json.getInt(object, "maxPoolIdleSize"))
                .readOnly(Json.getBoolean(object, "readOnly"))
                .autoCommit(Json.getBoolean(object, "autoCommit"))
                .queryTimeOutSeconds(Json.getInt(object, "queryTimeOutSeconds"))
                .maxWaitTimeMillis(Json.getInt(object, "maxWaitTimeMills"))
                .fetchSize(Json.getInt(object, "fetchSize"))
                .properties(properties)
                .build();
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy