/* Copyright 2014 The jeo project. 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 io.jeo.sql;
import io.jeo.util.Pair;
import io.jeo.util.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.List;
import java.util.Locale;
/**
* DB abstraction layer.
*
* @author Ian Schneider <ischneider@boundlessgeo.com>
*/
public abstract class Backend implements Closeable {
protected static Logger LOG = LoggerFactory.getLogger(Backend.class);
protected static String log(String sql, Object... params) {
if (LOG.isDebugEnabled()) {
if (params.length == 1 && params[0] instanceof Collection) {
params = ((Collection) params[0]).toArray();
}
StringBuilder log = new StringBuilder(sql);
if (params.length > 0) {
log.append("; ");
for (Object p : params) {
log.append(p).append(", ");
}
log.setLength(log.length() - 2);
}
LOG.debug(log.toString());
}
return sql;
}
protected final DbTypes dbTypes = new DbTypes();
/**
* Return true if the backend can run scripts loaded via
* Class.getResourceAsStream
*
* @return
*/
public boolean canRunScripts() {
return true;
}
/**
* The implementation should support closing any non-Closeable object passed
* in and does not need to check for nulls. Use {@see #closeSafe} instead
* of directly calling this method.
* @param object the non-null object to close
* @throws Exception if an error occurs closing
*/
protected abstract void closeInternal(Object object) throws Exception;
/**
* Close an object created by this Backend. Accepts null.
* @param o object or null
*/
public final void closeSafe(Object o) {
if (o != null) {
try {
if (o instanceof Closeable) {
((Closeable) o).close();
} else {
closeInternal(o);
}
} catch (Exception ex) {
LOG.warn("Error closing resource " + ex.getMessage() + " for " + o.getClass());
}
}
}
/**
* Get name and type information for the specified table.
* @param table the name of the table
* @return non-null list of column information
* @throws java.io.IOException if any errors occur
*/
public abstract List<Pair<String, Class>> getColumnInfo(String table) throws IOException;
/**
* Open a new Session.
* @return non-null Session ready for use
* @throws java.io.IOException if an error occurs
*/
public abstract Session session() throws IOException;
/**
* Execute a SQL statement calling {@link String.format} with the provided query and
* optional args.
* @param sql SQL to execute
* @param args optional arguments to use during formatting
* @throws java.io.IOException
*/
public final void exec(String sql, Object... args) throws IOException {
sql = String.format(Locale.ROOT, sql, args);
Session s = session();
try {
s.execute(sql);
} finally {
s.close();
}
}
/**
* Execute a SQL statement using the provided args.
* @param sql the query to execute
* @param args the arguments
* @throws java.io.IOException
*/
public final void execPrepared(String sql, Object... args) throws IOException {
Session s = session();
try {
s.executePrepared(sql, args);
} finally {
s.close();
}
}
void processScript(Session session, String file, StringBuilder buf) throws IOException {
List<String> lines = readScript(file);
for (String sql : lines) {
sql = sql.trim();
if (sql.isEmpty()) {
continue;
}
if (sql.startsWith("--")) {
continue;
}
buf.append(sql).append(" ");
if (sql.endsWith(";")) {
String stmt = buf.toString();
boolean skipError = stmt.startsWith("?");
if (skipError) {
stmt = stmt.replaceAll("^\\? *", "");
}
session.addBatch(stmt);
LOG.debug(stmt);
buf.setLength(0);
}
}
}
/**
* Execute a query calling {@link String.format} with the provided query and
* optional args.
* @param query the query to execute
* @param args the arguments
* @throws java.io.IOException
*/
public Results query(String query, Object... args) throws IOException {
String sql = String.format(Locale.ROOT, query, args);
Session s = session();
// chain the session to the query so it's closed, too
return s.query(sql).closeSession(s);
}
/**
* Execute a prepared query using the provided args.
* @param query the query to execute
* @param args the arguments
* @throws java.io.IOException
*/
public Results queryPrepared(String query, Object... args) throws IOException {
Session s = session();
// chain the session to the query so it's closed, too
return s.queryPrepared(query, args).closeSession(s);
}
List<String> readScript(String file) throws IOException {
InputStream in = getClass().getResourceAsStream(file);
BufferedReader r = new BufferedReader(new InputStreamReader(in, Util.UTF_8));
try {
List<String> lines = new ArrayList<String>();
String line = null;
while ((line = r.readLine()) != null) {
lines.add(line);
}
return lines;
} finally {
r.close();
}
}
public void runScripts(String... files) throws IOException {
Session session = session();
try {
StringBuilder buf = new StringBuilder();
for (String file : files) {
processScript(session, file, buf);
}
session.executeBatch();
} finally {
session.close();
}
}
/**
* Begin a Session using a transaction.
* @return Session
* @throws java.io.IOException
*/
public Session transaction() throws IOException {
Session session = session();
session.beginTransaction();
return session;
}
/**
* A Session is an API adapter to a DB backend query session.
*/
public abstract class Session implements Closeable {
private final Deque<Object> opened = new ArrayDeque<>();
public final <T> T open(T o) {
opened.push(o);
return o;
}
public final void close() {
while (!opened.isEmpty()) {
closeSafe(opened.pop());
}
}
/**
* Add batch SQL to execute. Must use {@see #executeBatch} to execute.
* An implementation may choose to execute here.
* @param sql
* @throws java.io.IOException
*/
public abstract void addBatch(String sql) throws IOException;
/**
* Execute any batch SQL added via {@see #addBatch}. An implementation
* may do nothing here if already executed.
* @throws java.io.IOException
*/
public abstract void executeBatch() throws IOException;
/**
* Execute a query using placeholders.
* @param sql the SQL with placeholders
* @param args the arguments
* @return non-null Results
* @throws java.io.IOException
*/
public abstract Results queryPrepared(String sql, Object... args) throws IOException;
/**
* Execute a query.
* @param sql the SQL to execute
* @return non-null Results
* @throws java.io.IOException
*/
public abstract Results query(String sql) throws IOException;
/**
* Execute a statement using placeholders.
* @param sql the SQL with placeholders
* @param args the arguments
* @throws java.io.IOException
*/
public abstract void executePrepared(String sql, Object... args) throws IOException;
/**
* Execute a query.
* @param sql the SQL to execute
* @throws java.io.IOException
*/
public abstract void execute(String sql) throws IOException;
/**
* End a transaction.
* @param complete true if success, false to rollback
* @throws java.io.IOException
*/
public abstract void endTransaction(boolean complete) throws IOException;
/**
* Start a transaction.
* @throws java.io.IOException
*/
public abstract void beginTransaction() throws IOException;
/**
* Get the primary keys for the table.
* @param tableName
* @return non-null List of primary keys
* @throws java.io.IOException
*/
public abstract List<String> getPrimaryKeys(String tableName) throws IOException;
}
/**
* Wrapper for database API result set or cursor. Column access is 0-based
* unlike JDBC.
*/
public abstract class Results implements Closeable {
private Session session;
/**
* Returns the current exception, throwing an exception if none set.
*/
public Session session() {
if (session == null) {
throw new IllegalStateException("No session set");
}
return session;
}
/**
* Attach the parent Session to be closed along with this Results.
* @param session the session to close
* @return this Results
*/
public final Results closeSession(Session session) {
this.session = session;
return this;
}
@Override
public final void close() {
try {
closeInternal();
} catch (Exception ex) {
LOG.warn("Error closing Results", ex);
}
closeSafe(session);
}
public abstract Object getObject(int idx, Class clazz) throws IOException;
public abstract String getString(int idx) throws IOException;
public abstract String getString(String col) throws IOException;
public abstract int getInt(int idx) throws IOException;
public abstract double getDouble(int idx) throws IOException;
public abstract long getLong(int idx) throws IOException;
public abstract boolean getBoolean(int idx) throws IOException;
public abstract byte[] getBytes(int idx) throws IOException;
/**
* Advance the current results row.
* @return true if another row exists
* @throws java.io.IOException if an error occurs
*/
public abstract boolean next() throws IOException;
/**
* Close underlying resources
* @throws Exception if an error occurs
*/
protected abstract void closeInternal() throws Exception;
}
}