package org.rapidoid.jdbc;
import org.rapidoid.annotation.Authors;
import org.rapidoid.annotation.Since;
import org.rapidoid.commons.Err;
import org.rapidoid.concurrent.Callback;
import org.rapidoid.concurrent.Callbacks;
import org.rapidoid.config.Conf;
import org.rapidoid.config.Config;
import org.rapidoid.datamodel.Results;
import org.rapidoid.datamodel.impl.ResultsImpl;
import org.rapidoid.group.AutoManageable;
import org.rapidoid.group.ManageableBean;
import org.rapidoid.io.Res;
import org.rapidoid.lambda.Mapper;
import org.rapidoid.lambda.Operation;
import org.rapidoid.log.Log;
import org.rapidoid.u.U;
import org.rapidoid.util.LazyInit;
import org.rapidoid.util.Msc;
import org.rapidoid.util.MscOpts;
import javax.sql.DataSource;
import java.sql.*;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
/*
* #%L
* rapidoid-sql
* %%
* Copyright (C) 2014 - 2017 Nikolche Mihajlovski and contributors
* %%
* 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.
* #L%
*/
@Authors("Nikolche Mihajlovski")
@Since("3.0.0")
@ManageableBean(kind = "jdbc")
public class JdbcClient extends AutoManageable<JdbcClient> {
private volatile boolean initialized;
private volatile String username;
private volatile String password;
private volatile String driver;
private volatile String url;
private volatile boolean usePool = true;
private volatile DataSource dataSource;
private volatile ReadWriteMode mode = ReadWriteMode.READ_WRITE;
private final Config config;
private final LazyInit<JdbcWorkers> workers = new LazyInit<>(new Callable<JdbcWorkers>() {
@Override
public JdbcWorkers call() throws Exception {
return new JdbcWorkers(JdbcClient.this);
}
});
public JdbcClient(String name) {
super(name);
this.config = Conf.JDBC.defaultOrCustom(name);
configure();
}
public void configure() {
url(config.entry("url").str().getOrNull());
username(config.entry("username").str().getOrNull());
password(config.entry("password").str().getOrNull());
driver(config.entry("driver").str().getOrNull());
if (U.isEmpty(driver) && U.notEmpty(url)) {
driver(inferDriverFromUrl(url));
}
}
public static String inferDriverFromUrl(String url) {
if (url.startsWith("jdbc:mysql:")) {
return "com.mysql.jdbc.Driver";
} else if (url.startsWith("jdbc:h2:")) {
return "org.hibernate.dialect.H2Dialect";
} else if (url.startsWith("jdbc:hsqldb:")) {
return "org.hsqldb.jdbc.JDBCDriver";
}
return null;
}
public synchronized JdbcClient username(String username) {
if (U.neq(this.username, username)) {
this.username = username;
this.initialized = false;
}
return this;
}
public synchronized JdbcClient password(String password) {
if (U.neq(this.password, password)) {
this.password = password;
this.initialized = false;
}
return this;
}
public synchronized JdbcClient driver(String driver) {
if (U.neq(this.driver, driver)) {
this.driver = driver;
this.initialized = false;
}
return this;
}
/**
* Use dataSource(...) instead.
*/
@Deprecated
public synchronized JdbcClient pool(DataSource pool) {
return dataSource(pool);
}
public synchronized JdbcClient dataSource(DataSource dataSource) {
if (U.neq(this.dataSource, dataSource)) {
this.dataSource = dataSource;
this.usePool = dataSource != null;
this.initialized = false;
}
return this;
}
public synchronized JdbcClient url(String url) {
if (U.neq(this.url, url)) {
this.url = url;
this.initialized = false;
}
return this;
}
public synchronized JdbcClient usePool(boolean usePool) {
if (U.neq(this.usePool, usePool)) {
this.usePool = usePool;
this.initialized = false;
}
return this;
}
public synchronized JdbcClient mode(ReadWriteMode mode) {
if (U.neq(this.mode, mode)) {
this.mode = mode;
this.initialized = false;
}
return this;
}
/**
* Use <code>usePool(true)</code> instead.
*/
@Deprecated
public JdbcClient pooled() {
usePool(true);
return this;
}
public JdbcClient mysql(String host, int port, String databaseName) {
return driver("com.mysql.jdbc.Driver").url(U.frmt("jdbc:mysql://%s:%s/%s", host, port, databaseName));
}
public JdbcClient h2(String databaseName) {
return driver("org.h2.Driver").url("jdbc:h2:mem:" + databaseName + ";DB_CLOSE_DELAY=-1").username("sa").password("");
}
public JdbcClient hsql(String databaseName) {
return driver("org.hsqldb.jdbc.JDBCDriver").url("jdbc:hsqldb:mem:" + databaseName).username("sa").password("");
}
private void registerJDBCDriver() {
if (driver == null && url != null) {
driver = inferDriverFromUrl(url);
}
validateArgNotNull("driver", driver);
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
throw U.rte("Cannot find JDBC driver class: " + driver);
}
}
private void validateArgNotNull(String argName, String argValue) {
if (argValue == null) {
throw U.rte("The JDBC parameter '" + argName + "' must be configured!");
}
}
private synchronized void ensureIsInitialized() {
if (!initialized) {
validate();
registerJDBCDriver();
if (this.dataSource == null) {
this.dataSource = this.usePool ? createPool() : null;
}
String maskedPassword = U.isEmpty(password) ? "<empty>" : "<specified>";
Log.info("Initialized JDBC API", "!url", url, "!driver", driver, "!username", username, "!password", maskedPassword, "!dataSource", dataSource);
initialized = true;
}
}
private DataSource createPool() {
if (MscOpts.hasC3P0()) return C3P0Factory.createDataSourceFor(this);
if (MscOpts.hasHikari()) return HikariFactory.createDataSourceFor(this);
throw U.rte("Cannot create JDBC connection pool, couldn't find Hikari nor C3P0!");
}
private void validate() {
U.must(U.notEmpty(username != null), "The database username must be specified!");
U.must(U.notEmpty(password != null), "The database password must be specified!");
U.must(U.notEmpty(url != null), "The database connection URL must be specified!");
U.must(U.notEmpty(driver != null), "The database driver must be specified!");
}
public Connection getConnection() {
ensureIsInitialized();
return provideConnection();
}
private static void close(Connection conn) {
try {
if (conn != null) conn.close();
} catch (SQLException e) {
throw U.rte("Error occurred while closing the connection!", e);
}
}
private static void close(PreparedStatement stmt) {
try {
if (stmt != null) stmt.close();
} catch (SQLException e) {
throw U.rte("Error occurred while closing the statement!", e);
}
}
private static void close(ResultSet rs) {
try {
if (rs != null) rs.close();
} catch (SQLException e) {
throw U.rte("Error occurred while closing the ResultSet!", e);
}
}
public int execute(String sql, Object... args) {
return doExecute(sql, null, args);
}
public int execute(String sql, Map<String, ?> namedArgs) {
return doExecute(sql, namedArgs, null);
}
private int doExecute(String sql, Map<String, ?> namedArgs, Object[] args) {
ensureIsInitialized();
sql = toSql(sql);
Log.debug("SQL", "sql", sql, "args", args);
Connection conn = provideConnection();
PreparedStatement stmt = null;
try {
stmt = JDBC.prepare(conn, sql, namedArgs, args);
String q = sql.trim().toUpperCase();
if (q.startsWith("INSERT ")
|| q.startsWith("UPDATE ")
|| q.startsWith("DELETE ")) {
return stmt.executeUpdate();
} else {
return stmt.execute() ? 1 : 0;
}
} catch (SQLException e) {
throw U.rte(e);
} finally {
close(stmt);
close(conn);
}
}
public int tryToExecute(String sql, Object... args) {
return doTryToExecute(sql, null, args);
}
public int tryToExecute(String sql, Map<String, ?> namedArgs) {
return doTryToExecute(sql, namedArgs, null);
}
private int doTryToExecute(String sql, Map<String, ?> namedArgs, Object[] args) {
try {
return doExecute(sql, namedArgs, args);
} catch (Exception e) {
// ignore the exception
Log.warn("Ignoring JDBC error", "error", Msc.errorMsg(e));
}
return 0;
}
public <T> Results<T> query(Class<T> resultType, String sql, Object... args) {
return doQuery(resultType, null, sql, null, args);
}
public <T> Results<T> query(Class<T> resultType, String sql, Map<String, ?> namedArgs) {
return doQuery(resultType, null, sql, namedArgs, null);
}
public <T> Results<T> query(Mapper<ResultSet, T> resultMapper, String sql, Object... args) {
return doQuery(null, resultMapper, sql, null, args);
}
public <T> Results<T> query(Mapper<ResultSet, T> resultMapper, String sql, Map<String, ?> namedArgs) {
return doQuery(null, resultMapper, sql, namedArgs, null);
}
private <T> Results<T> doQuery(Class<T> resultType, Mapper<ResultSet, T> resultMapper, String sql, Map<String, ?> namedArgs, Object[] args) {
sql = toSql(sql);
JdbcData<T> data = new JdbcData<>(this, resultType, resultMapper, sql, namedArgs, args);
return new ResultsImpl<>(data);
}
<T> List<T> runQuery(Class<T> resultType, Mapper<ResultSet, T> resultMapper, String sql, Map<String, ?> namedArgs, Object[] args, long start, long length) {
ensureIsInitialized();
U.must(start >= 0);
U.must(length >= 0);
if (start > 0 || length < Long.MAX_VALUE) {
// FIXME paging
throw Err.notReady();
}
Connection conn = provideConnection();
PreparedStatement stmt = null;
ResultSet rs = null;
try {
stmt = JDBC.prepare(conn, sql, namedArgs, args);
rs = stmt.executeQuery();
if (resultMapper != null) {
return JDBC.rows(resultMapper, rs);
} else {
if (resultType.equals(Map.class)) {
return U.cast(JDBC.rows(rs));
} else {
return JDBC.rows(resultType, rs);
}
}
} catch (Exception e) {
throw U.rte(e);
} finally {
close(rs);
close(stmt);
close(conn);
}
}
long getQueryCount(String sql, Map<String, ?> namedArgs, Object[] args) {
// FIXME find a better way
return -1; // unknown
}
private static String toSql(String sql) {
if (sql.endsWith(".sql")) {
sql = Res.from(sql).mustExist().getContent();
}
return sql;
}
public Results<Map<String, Object>> query(String sql, Object... args) {
return U.cast(query(Map.class, sql, args));
}
public Results<Map<String, Object>> query(String sql, Map<String, ?> namedArgs) {
return U.cast(query(Map.class, sql, namedArgs));
}
private Connection provideConnection() {
try {
if (dataSource != null) {
Connection conn = dataSource.getConnection();
U.notNull(conn, "JDBC connection");
return conn;
} else {
U.must(!usePool, "Expecting connection pool, but the data source is null!");
return getConnectionFromDriver();
}
} catch (SQLException e) {
throw U.rte("Cannot create JDBC connection!", e);
}
}
private Connection getConnectionFromDriver() throws SQLException {
if (username != null) {
String pass = U.safe(password);
return DriverManager.getConnection(url, username, pass);
} else {
return DriverManager.getConnection(url);
}
}
public void release(Connection connection) {
try {
connection.close();
} catch (SQLException e) {
Log.error("Error while releasing a JDBC connection!", e);
}
}
public String username() {
return username;
}
public String password() {
return password;
}
public String driver() {
return driver;
}
public String url() {
return url;
}
/**
* Use dataSource() instead.
*/
@Deprecated
public DataSource pool() {
return dataSource();
}
public DataSource dataSource() {
return dataSource;
}
public boolean usePool() {
return usePool;
}
public ReadWriteMode mode() {
return mode;
}
public JdbcClient init() {
ensureIsInitialized();
return this;
}
@Override
public String toString() {
return "JdbcClient{" +
"initialized=" + initialized +
", username='" + username + '\'' +
", password='" + "*" + '\'' +
", driver='" + driver + '\'' +
", url='" + url + '\'' +
", usePool=" + usePool +
", pool=" + dataSource +
", mode=" + mode +
'}';
}
public void execute(Operation<Connection> operation) {
workers.get().execute(operation);
}
public void execute(final Callback<Void> callback, final Operation<Connection> operation) {
execute(new Operation<Connection>() {
@Override
public void execute(Connection conn) {
try {
operation.execute(conn);
} catch (Throwable e) {
Callbacks.done(callback, null, e);
return;
}
Callbacks.done(callback, null, null);
}
});
}
public <T> void execute(final Mapper<ResultSet, T> resultMapper, final Callback<List<T>> callback, final String sql, final Object... args) {
execute(new Operation<Connection>() {
@Override
public void execute(Connection conn) throws Exception {
List<T> results = U.list();
try (PreparedStatement stmt = JdbcUtil.prepare(conn, sql, null, args)) {
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
results.add(resultMapper.map(rs));
}
} catch (Throwable e) {
Callbacks.done(callback, null, e);
return;
}
Callbacks.done(callback, results, null);
}
});
}
}