/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.jooby.jdbc; import static com.google.common.base.Preconditions.checkArgument; import static java.util.Objects.requireNonNull; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.Properties; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import javax.sql.DataSource; import org.jooby.Env; import org.jooby.Jooby; import com.google.common.base.CharMatcher; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.inject.Binder; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import com.typesafe.config.ConfigValue; import com.typesafe.config.ConfigValueFactory; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import javaslang.Function3; import javaslang.Tuple; import javaslang.Tuple2; import javaslang.control.Try; /** * <h1>jdbc</h1> * <p> * Production-ready jdbc data source, powered by the * <a href="https://github.com/brettwooldridge/HikariCP">HikariCP</a> library. * </p> * * <h2>usage</h2> * * Via connection string: * <pre> * { * use(new Jdbc("jdbc:mysql://localhost/db")); * * // accessing to the data source * get("/my-api", (req, rsp) {@literal ->} { * DataSource db = req.getInstance(DataSource.class); * // do something with datasource * }); * } * </pre> * * Via <code>db</code> property: * <pre> * { * use(new Jdbc("db")); * * // accessing to the data source * get("/my-api", (req, rsp) {@literal ->} { * DataSource db = req.getInstance(DataSource.class); * // do something with datasource * }); * } * </pre> * * <h2>db configuration</h2> * <p> * Database configuration is controlled from your <code>application.conf</code> file using the * <code>db</code> property and friends: <code>db.*</code>. * </p> * * <h3>mem db</h3> * * <pre> * db = mem * </pre> * * Mem db is implemented with <a href="http://www.h2database.com/">h2 database</a>, before using it * make sure to add the h2 dependency to your <code>pom.xml</code>: * * <pre> * <dependency> * <groupId>com.h2database</groupId> * <artifactId>h2</artifactId> * </dependency> * </pre> * * Mem db is useful for dev environment and/or transient data that can be regenerated. * * <h3>fs db</h3> * * <pre> * db = fs * </pre> * * File system db is implemented with <a href="http://www.h2database.com/">h2 database</a>, before * using it make sure to add the h2 dependency to your ```pom.xml```: * * File system db is useful for dev environment and/or transient data that can be * regenerated. Keep in mind this db is saved in a tmp directory and db will be deleted it * on restarts. * * <h3>db.url</h3> * <p> * Connect to a database using a jdbc url, some examples here: * </p> * * <pre> * # mysql * db.url = jdbc:mysql://localhost/mydb * db.user=myuser * db.password=password * </pre> * * Previous example, show you how to connect to <strong>mysql</strong>, setting user and password. * But of course you need the jdbc driver on your <code>pom.xml</code>: * * <h3>hikari configuration</h3> * <p> * If you need to configure or tweak the <a * href="https://github.com/brettwooldridge/HikariCP">hikari pool</a> just add <code>hikari.*</code> * entries to your <code>application.conf</code> file: * </p> * * <pre> * db.url = jdbc:mysql://localhost/mydb * db.user=myuser * db.password=password * db.cachePrepStmts=true * * # hikari * hikari.autoCommit = true * hikari.maximumPoolSize = 20 * # etc... * </pre> * * <p> * Also, all the <code>db.*</code> properties are converted to <code>dataSource.*</code> to let <a * href="https://github.com/brettwooldridge/HikariCP">hikari</a> configure the target jdbc * connection. * </p> * * <h2>multiple connections</h2> * It is pretty simple to configure two or more db connections. * * Let's suppose we have a main database and an audit database for tracking changes: * * <pre> * { * use(new Jdbc("db.main")); // main database * use(new Jdbc("db.audit")); // audit database * } * </pre> * * <p> * application.conf: * </p> * * <pre> * # main database * db.main.url = ... * db.main.user=... * db.main.password = ... * * # audit * db.audit.url = .... * db.audit.user = .... * db.audit.password = .... * </pre> * * <p> * Same principle applies if you need to tweak hikari: * </p> * * <pre> * # max pool size for main db * hikari.main.maximumPoolSize = 100 * * # max pool size for audit db * hikari.audit.maximumPoolSize = 20 * </pre> * * <p> * Finally, if you need to inject the audit data source, all you have to do is to use the * <strong>Name</strong> annotation, like <code>@Name("db.audit")</code> * </p> * * * That's all folks! Enjoy it!!! * * @author edgar * @since 0.1.0 */ public class Jdbc implements Jooby.Module { static final Function<? super Throwable, ? extends Try<? extends Void>> CCE = x -> { if (x instanceof ClassCastException) { StackTraceElement src = x.getStackTrace()[0]; if (src.getFileName() == null || src.getClassName().equals(Jdbc.class.getName())) { return Try.success(null); } } return Try.failure(x); }; public static Function<String, String> DB_NAME = url -> { Function3<String, String, String, Tuple2<String, Map<String, String>>> indexOf = (str, t1, t2) -> { int i = str.indexOf(t1); int len = i >= 0 ? i : str.length() - 1; Map<String, String> params = Splitter.on(t2) .trimResults() .omitEmptyStrings() .withKeyValueSeparator('=') .split(str.substring(len + 1)); return Tuple.of(str.substring(0, len + 1), params); }; // strip ; or ? Tuple2<String, Map<String, String>> result = indexOf.apply(url, "?", "&"); Map<String, String> params = new HashMap<>(result._2); result = indexOf.apply(result._1, ";", ";"); params.putAll(result._2); List<String> parts = Splitter.on(CharMatcher.JAVA_LETTER_OR_DIGIT.negate()) .trimResults() .omitEmptyStrings() .splitToList(result._1); return Optional.ofNullable(params.get("database")) .orElse(Optional.ofNullable(params.get("databaseName")) .orElse(parts.get(parts.size() - 1))); }; @SuppressWarnings("rawtypes") private final List<BiConsumer> callback = new ArrayList<>(); private final String dbref; protected Optional<String> dbtype; /** * Creates a new {@link Jdbc} module. * * @param name A connection string or property with a connection string. */ public Jdbc(final String name) { checkArgument(name != null && name.length() > 0, "Connection String/Database property required."); this.dbref = name; } /** * Creates a new {@link Jdbc} module. The <code>db</code> property must be present in your * <code>.conf</code> file. */ public Jdbc() { this("db"); } /** * Configurer callback to apply advanced configuration while bootstrapping hibernate: * * <pre>{@code * { * use(new Jdbc() * .doWith((HikariConfig conf) -> { * // do with conf * }) * .doWith((HikariDataSource ds) -> { * // do with ds * }) * ); * } * }</pre> * * @param configurer Configurer callback. * @return This module */ public <T> Jdbc doWith(final BiConsumer<T, Config> configurer) { this.callback.add(requireNonNull(configurer, "Configurer required.")); return this; } /** * Configurer callback to apply advanced configuration while bootstrapping hibernate: * * <pre>{@code * { * use(new Jdbc() * .doWith((HikariConfig conf) -> { * // do with conf * }) * .doWith((HikariDataSource ds) -> { * // do with ds * }) * ); * } * }</pre> * * @param configurer Configurer callback. * @return This module */ public <T> Jdbc doWith(final Consumer<T> configurer) { requireNonNull(configurer, "Configurer required."); return doWith((final T b, final Config c) -> configurer.accept(b)); } @Override public void configure(final Env env, final Config config, final Binder binder) { configure(env, config, binder, (name, ds) -> { }); } protected void configure(final Env env, final Config config, final Binder binder, final BiConsumer<String, HikariDataSource> extensions) { Config dbconf; String url, dbname, dbkey; boolean seturl = false; if (dbref.startsWith("jdbc:")) { dbconf = config; url = dbref; dbname = DB_NAME.apply(url); dbkey = dbname; seturl = true; } else { dbconf = dbConfig(dbref, config); url = dbconf.getString(dbref + ".url"); dbname = DB_NAME.apply(url); dbkey = dbref; } HikariConfig hikariConf = hikariConfig(url, dbkey, dbname, dbconf); if (seturl) { Properties props = hikariConf.getDataSourceProperties(); props.setProperty("url", url); } callback(hikariConf, config); HikariDataSource ds = new HikariDataSource(hikariConf); extensions.accept(dbname, ds); env.serviceKey() .generate(DataSource.class, dbname, k -> binder.bind(k).toInstance(ds)); env.onStop(ds::close); } @Override public Config config() { return ConfigFactory.parseResources(Jdbc.class, "jdbc.conf"); } private Config dbConfig(final String key, final Config source) { Object db = source.getAnyRef(key); if (db instanceof String) { // embedded db? return Try.of(() -> source.getConfig("databases." + db)) .map(it -> { // Rewrite embedded db Config dbtree = it.withValue("url", ConfigValueFactory.fromAnyRef( it.getString("url").replace("{mem.seed}", System.currentTimeMillis() + ""))); // write embedded with current key return ConfigFactory.empty() .withValue(key, dbtree.root()) .withFallback(source); }).getOrElse(() -> { // assume it is a just the url return ConfigFactory.empty() .withValue(key + ".url", ConfigValueFactory.fromAnyRef(db.toString())) .withFallback(source); }); } else { return source; } } private HikariConfig hikariConfig(final String url, final String key, final String db, final Config config) { Properties props = new Properties(); BiConsumer<String, Entry<String, ConfigValue>> dumper = (prefix, entry) -> { String propertyName = prefix + entry.getKey(); String propertyValue = entry.getValue().unwrapped().toString(); props.setProperty(propertyName, propertyValue); }; Function<String, Config> dbconf = path -> Try.of(() -> config.getConfig(path)) .getOrElse(ConfigFactory.empty()); Config $hikari = dbconf.apply(key + ".hikari") .withFallback(dbconf.apply("db." + db + ".hikari")) .withFallback(dbconf.apply("hikari")); // figure it out db type. dbtype = dbtype(url, config); /** * dump properties from less to higher precedence * * # databases.[type] * # db.* -> dataSource.* * # hikari.* -> * (no prefix) */ dbtype.ifPresent(type -> config.getConfig("databases." + type) .entrySet().forEach(entry -> dumper.accept("dataSource.", entry))); dbconf.apply(key) .withoutPath("hikari") .entrySet().forEach(entry -> dumper.accept("dataSource.", entry)); $hikari.entrySet().forEach(entry -> dumper.accept("", entry)); String dataSourceClassName = props.getProperty("dataSourceClassName"); if (Strings.isNullOrEmpty(dataSourceClassName)) { // adjust dataSourceClassName when missing dataSourceClassName = props.getProperty("dataSource.dataSourceClassName"); props.setProperty("dataSourceClassName", dataSourceClassName); } // remove dataSourceClassName under dataSource props.remove("dataSource.dataSourceClassName"); // set pool name props.setProperty("poolName", dbtype.map(type -> type + "." + db).orElse(db)); return new HikariConfig(props); } @SuppressWarnings("unchecked") protected void callback(final Object value, final Config conf) { this.callback.forEach(it -> Try.run(() -> it.accept(value, conf)) .recoverWith(CCE) .getOrElseThrow(Throwables::propagate)); } private Optional<String> dbtype(final String url, final Config config) { String type = Arrays.stream(url.toLowerCase().split(":")) .filter(token -> !(token.equals("jdbc") || token.equals("jtds"))) .findFirst() .get(); return Optional.of(type); } }