package org.basex.query.func;
import static java.sql.DriverManager.*;
import static org.basex.query.QueryText.*;
import static org.basex.query.util.Err.*;
import static org.basex.util.Token.*;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.Properties;
import org.basex.query.QueryContext;
import org.basex.query.QueryException;
import org.basex.query.expr.Expr;
import org.basex.query.item.ANode;
import org.basex.query.item.AtomType;
import org.basex.query.item.Bln;
import org.basex.query.item.FAttr;
import org.basex.query.item.FElem;
import org.basex.query.item.FTxt;
import org.basex.query.item.Item;
import org.basex.query.item.Int;
import org.basex.query.item.NodeType;
import org.basex.query.item.QNm;
import org.basex.query.item.SeqType;
import org.basex.query.item.map.Map;
import org.basex.query.iter.AxisIter;
import org.basex.query.iter.AxisMoreIter;
import org.basex.query.iter.Iter;
import org.basex.query.iter.NodeCache;
import org.basex.util.Atts;
import org.basex.util.InputInfo;
import org.basex.util.Reflect;
import org.basex.util.hash.TokenObjMap;
/**
* Functions on relational databases.
* @author BaseX Team 2005-12, BSD License
* @author Rositsa Shadura
*/
public final class FNSql extends StandardFunc {
/** Types. */
/** Type int. */
private static final byte[] INT = AtomType.INT.string();
/** Type string. */
private static final byte[] STRING = AtomType.STR.string();
/** Type boolean. */
private static final byte[] BOOL = AtomType.BLN.string();
/** Type date. */
private static final byte[] DATE = AtomType.DAT.string();
/** Type double. */
private static final byte[] DOUBLE = AtomType.DBL.string();
/** Type float. */
private static final byte[] FLOAT = AtomType.FLT.string();
/** Type short. */
private static final byte[] SHORT = AtomType.SHR.string();
/** Type time. */
private static final byte[] TIME = AtomType.TIM.string();
/** Type timestamp. */
private static final byte[] TIMESTAMP = token("timestamp");
/** Names. */
/** Row. */
private static final QNm Q_ROW = new QNm(token("sql:row"), SQLURI);
/** Column. */
private static final QNm Q_COLUMN = new QNm(token("sql:column"), SQLURI);
/** Name. */
private static final QNm Q_NAME = new QNm(token("name"), EMPTY);
/** Elements. */
/** <sql:options/>. */
private static final QNm E_OPS = new QNm(token("options"), SQLURI);
/** <sql:parameters/>. */
private static final QNm E_PARAMS = new QNm(token("parameters"), SQLURI);
/** <sql:parameter/>. */
private static final QNm E_PARAM = new QNm(token("parameter"), SQLURI);
/** Connection options. */
/** Auto-commit mode. */
private static final byte[] AUTO_COMM = token("autocommit");
/** User. */
private static final String USER = "user";
/** Password. */
private static final String PASS = "password";
/** Other. */
/** SQL Namespace attribute. */
private static final Atts NS_SQL = new Atts(SQL, SQLURI);
/** Attribute "type" of <sql:parameter/>. */
private static final byte[] TYPE = token("type");
/**
* Constructor.
* @param ii input info
* @param f function definition
* @param e arguments
*/
public FNSql(final InputInfo ii, final Function f, final Expr... e) {
super(ii, f, e);
}
@Override
public Iter iter(final QueryContext ctx) throws QueryException {
checkAdmin(ctx);
switch(sig) {
case _SQL_EXECUTE: return execute(ctx);
default: return super.iter(ctx);
}
}
@Override
public Item item(final QueryContext ctx, final InputInfo ii)
throws QueryException {
checkAdmin(ctx);
switch(sig) {
case _SQL_INIT: return init(ctx);
case _SQL_CONNECT: return connect(ctx);
case _SQL_PREPARE: return prepare(ctx);
case _SQL_CLOSE: return close(ctx);
case _SQL_COMMIT: return commit(ctx);
case _SQL_ROLLBACK: return rollback(ctx);
default: return super.item(ctx, ii);
}
}
/**
* Initializes JDBC with the specified driver.
* @param ctx query context
* @return {@code null}
* @throws QueryException query exception
*/
private Item init(final QueryContext ctx) throws QueryException {
final String driver = string(checkStr(expr[0], ctx));
if(Reflect.find(driver) == null) SQLINIT.thrw(input, driver);
return null;
}
/**
* Establishes a connection to a relational database.
* @param ctx query context
* @return connection id
* @throws QueryException query exception
*/
private Int connect(final QueryContext ctx) throws QueryException {
// URL to relational database
final String url = string(checkStr(expr[0], ctx));
try {
if(expr.length > 2) {
// Credentials
final String user = string(checkStr(expr[1], ctx));
final String pass = string(checkStr(expr[2], ctx));
if(expr.length == 4) {
// Connection options
final TokenObjMap<Object> options = options(3, E_OPS, ctx);
boolean autoCommit = true;
final Object commit = options.get(AUTO_COMM);
if(commit != null) {
// Extract auto-commit mode from options
autoCommit = Boolean.parseBoolean(commit.toString());
options.delete(AUTO_COMM);
}
// Connection properties
final Properties props = connProps(options(3, E_OPS, ctx));
props.setProperty(USER, user);
props.setProperty(PASS, pass);
// Open connection
final Connection conn = getConnection(url, props);
// Set auto/commit mode
conn.setAutoCommit(autoCommit);
return Int.get(ctx.jdbc().add(conn));
}
return Int.get(ctx.jdbc().add(getConnection(url, user, pass)));
}
return Int.get(ctx.jdbc().add(getConnection(url)));
} catch(final SQLException ex) {
throw SQLEXC.thrw(input, ex.getMessage());
}
}
/**
* Parses connection options.
* @param options options
* @return connection properties
*/
private static Properties connProps(final TokenObjMap<Object> options) {
final Properties props = new Properties();
for(final byte[] next : options.keys()) {
if(next != null) props.setProperty(string(next),
options.get(next).toString());
}
return props;
}
/**
* Prepares a statement and returns its id.
* @param ctx query context
* @return prepared statement id
* @throws QueryException query exception
*/
private Int prepare(final QueryContext ctx) throws QueryException {
final Connection conn = connection(ctx, false);
// Prepared statement
final byte[] prepStmt = checkStr(expr[1], ctx);
try {
// Keep prepared statement
final PreparedStatement prep = conn.prepareStatement(string(prepStmt));
return Int.get(ctx.jdbc().add(prep));
} catch(final SQLException ex) {
throw SQLEXC.thrw(input, ex.getMessage());
}
}
/**
* Executes a query, update or prepared statement.
* @param ctx query context
* @return result
* @throws QueryException query exception
*/
private Iter execute(final QueryContext ctx) throws QueryException {
final int id = (int) checkItr(expr[0].item(ctx, input));
final Object obj = ctx.jdbc().get(id);
if(obj == null) throw NOCONN.thrw(input, id);
// Execute query or prepared statement
return obj instanceof Connection ? executeQuery((Connection) obj, ctx)
: executePrepStmt((PreparedStatement) obj, ctx);
}
/**
* Executes a query or an update statement on a relational database.
* @param conn connection
* @param ctx query context
* @return result
* @throws QueryException query exception
*/
private NodeCache executeQuery(final Connection conn, final QueryContext ctx)
throws QueryException {
final String query = string(checkStr(ctx.iter(expr[1]).next(), ctx));
Statement stmt = null;
try {
stmt = conn.createStatement();
final boolean result = stmt.execute(query);
return result ? buildResult(stmt.getResultSet()) : new NodeCache();
} catch(final SQLException ex) {
throw SQLEXC.thrw(input, ex.getMessage());
} finally {
if(stmt != null) try { stmt.close(); } catch(final SQLException ex) { }
}
}
/**
* Executes a prepared statement.
* @param stmt prepared statement
* @param ctx query context
* @return result
* @throws QueryException query exception
*/
private NodeCache executePrepStmt(final PreparedStatement stmt,
final QueryContext ctx) throws QueryException {
// Get parameters for prepared statement
final ANode params = (ANode) checkType(expr[1].item(ctx, input),
NodeType.ELM);
if(!params.qname().eq(E_PARAMS)) PARWHICH.thrw(input, params.qname());
try {
final int placeCount = stmt.getParameterMetaData().getParameterCount();
// Check if number of parameters equals number of place holders
if(placeCount != countParams(params)) PARAMS.thrw(input);
else setParameters(params.children(), stmt);
final boolean result = stmt.execute();
return result ? buildResult(stmt.getResultSet()) : new NodeCache();
} catch(final SQLException ex) {
throw SQLEXC.thrw(input, ex.getMessage());
}
}
/**
* Counts the numbers of <sql:parameter/> elements.
* @param params element <sql:parameter/>
* @return number of parameters
*/
private static long countParams(final ANode params) {
final AxisIter ch = params.children();
long c = ch.size();
if(c == -1) do
++c;
while(ch.next() != null);
return c;
}
/**
* Sets the parameters of a prepared statement.
* @param params parameters
* @param stmt prepared statement
* @throws QueryException query exception
*/
private void setParameters(final AxisMoreIter params,
final PreparedStatement stmt) throws QueryException {
int i = 0;
for(ANode next; (next = params.next()) != null;) {
// Check name
if(!next.qname().eq(E_PARAM)) PARWHICH.thrw(input, next.qname());
final AxisIter attrs = next.attributes();
byte[] paramType = null;
boolean isNull = false;
for(ANode attr; (attr = attrs.next()) != null;) {
// Attribute "type"
if(eq(attr.name(), TYPE)) paramType = attr.string();
// Attribute "null"
else if(eq(attr.name(), NULL))
isNull = attr.string() != null && Bln.parse(attr.string(), input);
// Not expected attribute
else throw NOTEXPATTR.thrw(input, string(attr.name()));
}
if(paramType == null) NOPARAMTYPE.thrw(input);
final byte[] v = next.string();
isNull |= v.length == 0;
setParam(++i, stmt, paramType, isNull ? null : string(v), isNull);
}
}
/**
* Sets the parameter with the given index in a prepared statement.
* @param index parameter index
* @param stmt prepared statement
* @param paramType parameter type
* @param value parameter value
* @param isNull indicator if the parameter is null or not
* @throws QueryException query exception
*/
private void setParam(final int index, final PreparedStatement stmt,
final byte[] paramType, final String value, final boolean isNull)
throws QueryException {
try {
if(eq(BOOL, paramType)) {
if(isNull) stmt.setNull(index, Types.BOOLEAN);
else stmt.setBoolean(index, Boolean.parseBoolean(value));
} else if(eq(DATE, paramType)) {
if(isNull) stmt.setNull(index, Types.DATE);
else stmt.setDate(index, Date.valueOf(value));
} else if(eq(DOUBLE, paramType)) {
if(isNull) stmt.setNull(index, Types.DOUBLE);
else stmt.setDouble(index, Double.parseDouble(value));
} else if(eq(FLOAT, paramType)) {
if(isNull) stmt.setNull(index, Types.FLOAT);
else stmt.setFloat(index, Float.parseFloat(value));
} else if(eq(INT, paramType)) {
if(isNull) stmt.setNull(index, Types.INTEGER);
else stmt.setInt(index, Integer.parseInt(value));
} else if(eq(SHORT, paramType)) {
if(isNull) stmt.setNull(index, Types.SMALLINT);
else stmt.setShort(index, Short.parseShort(value));
} else if(eq(STRING, paramType)) {
if(isNull) stmt.setNull(index, Types.VARCHAR);
else stmt.setString(index, value);
} else if(eq(TIME, paramType)) {
if(isNull) stmt.setNull(index, Types.TIME);
else stmt.setTime(index, Time.valueOf(value));
} else if(eq(TIMESTAMP, paramType)) {
if(isNull) stmt.setNull(index, Types.TIMESTAMP);
else stmt.setTimestamp(index, Timestamp.valueOf(value));
} else {
throw SQLEXC.thrw(input, "unsupported type: " + string(paramType));
}
} catch(final SQLException ex) {
throw SQLEXC.thrw(input, ex.getMessage());
} catch(final IllegalArgumentException ex) {
throw ILLFORMAT.thrw(input, string(paramType));
}
}
/**
* Builds a sequence of elements from a query's result set.
* @param rs result set
* @return sequence of elements <tuple/> each of which represents a row from
* the result set
* @throws QueryException query exception
*/
private NodeCache buildResult(final ResultSet rs) throws QueryException {
try {
final ResultSetMetaData metadata = rs.getMetaData();
final int columnCount = metadata.getColumnCount();
final NodeCache rows = new NodeCache();
while(rs.next()) {
final NodeCache columns = new NodeCache();
for(int k = 1; k <= columnCount; k++) {
// For each row add column values as children
final String label = metadata.getColumnLabel(k);
final Object value = rs.getObject(label);
// Null values are ignored
if(value != null) {
// Column name
final FAttr columnName = new FAttr(Q_NAME, token(label));
final NodeCache attr = new NodeCache();
attr.add(columnName);
// Column value
final FTxt columnValue = new FTxt(token(value.toString()));
final NodeCache ch = new NodeCache();
ch.add(columnValue);
// Element <sql:column name='...'>...</sql:column>
columns.add(new FElem(Q_COLUMN, ch, attr, NS_SQL));
}
}
rows.add(new FElem(Q_ROW, columns, null, NS_SQL));
}
return rows;
} catch(final SQLException ex) {
throw SQLEXC.thrw(input, ex.getMessage());
}
}
/**
* Closes a connection to a relational database.
* @param ctx query context
* @return {@code null}
* @throws QueryException query exception
*/
private Item close(final QueryContext ctx) throws QueryException {
try {
connection(ctx, true).close();
return null;
} catch(final SQLException ex) {
throw SQLEXC.thrw(input, ex.getMessage());
}
}
/**
* Commits all changes made during last transaction.
* @param ctx query context
* @return {@code null}
* @throws QueryException query exception
*/
private Item commit(final QueryContext ctx) throws QueryException {
try {
connection(ctx, false).commit();
return null;
} catch(final SQLException ex) {
throw SQLEXC.thrw(input, ex.getMessage());
}
}
/**
* Rollbacks all changes made during last transaction.
* @param ctx query context
* @return {@code null}
* @throws QueryException query exception
*/
private Item rollback(final QueryContext ctx) throws QueryException {
try {
connection(ctx, false).rollback();
return null;
} catch(final SQLException ex) {
throw SQLEXC.thrw(input, ex.getMessage());
}
}
/**
* Returns a connection and removes it from list with opened connections if
* requested.
* @param ctx query context
* @param del flag indicating if connection has to be removed
* @return connection
* @throws QueryException query exception
*/
private Connection connection(final QueryContext ctx, final boolean del)
throws QueryException {
final int id = (int) checkItr(expr[0].item(ctx, input));
final Object obj = ctx.jdbc().get(id);
if(obj == null || !(obj instanceof Connection)) NOCONN.thrw(input, id);
if(del) ctx.jdbc().remove(id);
return (Connection) obj;
}
/**
* Extracts connection options.
* @param arg argument with options
* @param root expected root element
* @param ctx query context
* @return options
* @throws QueryException query exception
*/
private TokenObjMap<Object> options(final int arg, final QNm root,
final QueryContext ctx) throws QueryException {
// initialize token map
final TokenObjMap<Object> tm = new TokenObjMap<Object>();
// argument does not exist...
if(arg >= expr.length) return tm;
// empty sequence...
final Item it = expr[arg].item(ctx, input);
if(it == null) return tm;
// XQuery map: convert to internal map
if(it instanceof Map) return ((Map) it).tokenJavaMap(input);
// no element: convert XQuery map to internal map
if(!it.type().eq(SeqType.ELM)) throw NODFUNTYPE.thrw(input, this, it.type);
// parse nodes
ANode node = (ANode) it;
if(!node.qname().eq(root)) PARWHICH.thrw(input, node.qname());
// interpret query parameters
final AxisIter ai = node.children();
while((node = ai.next()) != null) {
final QNm qn = node.qname();
if(!eq(qn.uri(), SQLURI)) PARWHICH.thrw(input, qn);
tm.add(qn.local(), node.children().next());
}
return tm;
}
@Override
public boolean uses(final Use u) {
return u == Use.NDT || super.uses(u);
}
}