510 lines
18 KiB
Java
510 lines
18 KiB
Java
/*
|
|
* Copyright (c) 2012-2015: Christopher J. Brody (aka Chris Brody)
|
|
* Copyright (c) 2005-2010, Nitobi Software Inc.
|
|
* Copyright (c) 2010, IBM Corporation
|
|
*/
|
|
|
|
package io.liteglue;
|
|
|
|
import android.annotation.SuppressLint;
|
|
|
|
import android.database.Cursor;
|
|
import android.database.CursorWindow;
|
|
import android.database.sqlite.SQLiteCursor;
|
|
import android.database.sqlite.SQLiteDatabase;
|
|
import android.database.sqlite.SQLiteException;
|
|
import android.database.sqlite.SQLiteStatement;
|
|
|
|
import android.util.Base64;
|
|
import android.util.Log;
|
|
|
|
import java.io.File;
|
|
import java.lang.IllegalArgumentException;
|
|
import java.lang.Number;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
|
|
import org.apache.cordova.CallbackContext;
|
|
|
|
import org.json.JSONArray;
|
|
import org.json.JSONException;
|
|
import org.json.JSONObject;
|
|
|
|
/**
|
|
* Android Database helper class
|
|
*/
|
|
class SQLiteAndroidDatabase
|
|
{
|
|
private static final Pattern FIRST_WORD = Pattern.compile("^\\s*(\\S+)",
|
|
Pattern.CASE_INSENSITIVE);
|
|
|
|
private static final Pattern WHERE_CLAUSE = Pattern.compile("\\s+WHERE\\s+(.+)$",
|
|
Pattern.CASE_INSENSITIVE);
|
|
|
|
private static final Pattern UPDATE_TABLE_NAME = Pattern.compile("^\\s*UPDATE\\s+(\\S+)",
|
|
Pattern.CASE_INSENSITIVE);
|
|
|
|
private static final Pattern DELETE_TABLE_NAME = Pattern.compile("^\\s*DELETE\\s+FROM\\s+(\\S+)",
|
|
Pattern.CASE_INSENSITIVE);
|
|
|
|
File dbFile;
|
|
|
|
SQLiteDatabase mydb;
|
|
|
|
/**
|
|
* NOTE: Using default constructor, no explicit constructor.
|
|
*/
|
|
|
|
/**
|
|
* Open a database.
|
|
*
|
|
* @param dbfile The database File specification
|
|
*/
|
|
void open(File dbfile) throws Exception {
|
|
dbFile = dbfile; // for possible bug workaround
|
|
mydb = SQLiteDatabase.openOrCreateDatabase(dbfile, null);
|
|
}
|
|
|
|
/**
|
|
* Close a database (in the current thread).
|
|
*/
|
|
void closeDatabaseNow() {
|
|
if (mydb != null) {
|
|
mydb.close();
|
|
mydb = null;
|
|
}
|
|
}
|
|
|
|
void bugWorkaround() throws Exception {
|
|
this.closeDatabaseNow();
|
|
this.open(dbFile);
|
|
}
|
|
|
|
/**
|
|
* Executes a batch request and sends the results via cbc.
|
|
*
|
|
* @param dbname The name of the database.
|
|
* @param queryarr Array of query strings
|
|
* @param jsonparams Array of JSON query parameters
|
|
* @param queryIDs Array of query ids
|
|
* @param cbc Callback context from Cordova API
|
|
*/
|
|
@SuppressLint("NewApi")
|
|
void executeSqlBatch(String[] queryarr, JSONArray[] jsonparams,
|
|
String[] queryIDs, CallbackContext cbc) {
|
|
|
|
if (mydb == null) {
|
|
// not allowed - can only happen if someone has closed (and possibly deleted) a database and then re-used the database
|
|
cbc.error("database has been closed");
|
|
return;
|
|
}
|
|
|
|
String query = "";
|
|
String query_id = "";
|
|
int len = queryarr.length;
|
|
JSONArray batchResults = new JSONArray();
|
|
|
|
for (int i = 0; i < len; i++) {
|
|
int rowsAffectedCompat = 0;
|
|
boolean needRowsAffectedCompat = false;
|
|
query_id = queryIDs[i];
|
|
|
|
JSONObject queryResult = null;
|
|
String errorMessage = "unknown";
|
|
|
|
try {
|
|
boolean needRawQuery = true;
|
|
|
|
query = queryarr[i];
|
|
|
|
QueryType queryType = getQueryType(query);
|
|
|
|
if (queryType == QueryType.update || queryType == queryType.delete) {
|
|
if (android.os.Build.VERSION.SDK_INT >= 11) {
|
|
SQLiteStatement myStatement = mydb.compileStatement(query);
|
|
|
|
if (jsonparams != null) {
|
|
bindArgsToStatement(myStatement, jsonparams[i]);
|
|
}
|
|
|
|
int rowsAffected = -1; // (assuming invalid)
|
|
|
|
// Use try & catch just in case android.os.Build.VERSION.SDK_INT >= 11 is lying:
|
|
try {
|
|
rowsAffected = myStatement.executeUpdateDelete();
|
|
// Indicate valid results:
|
|
needRawQuery = false;
|
|
} catch (SQLiteException ex) {
|
|
// Indicate problem & stop this query:
|
|
ex.printStackTrace();
|
|
errorMessage = ex.getMessage();
|
|
Log.v("executeSqlBatch", "SQLiteStatement.executeUpdateDelete(): Error=" + errorMessage);
|
|
needRawQuery = false;
|
|
} catch (Exception ex) {
|
|
// Assuming SDK_INT was lying & method not found:
|
|
// do nothing here & try again with raw query.
|
|
}
|
|
|
|
if (rowsAffected != -1) {
|
|
queryResult = new JSONObject();
|
|
queryResult.put("rowsAffected", rowsAffected);
|
|
}
|
|
} else { // pre-honeycomb
|
|
rowsAffectedCompat = countRowsAffectedCompat(queryType, query, jsonparams, mydb, i);
|
|
needRowsAffectedCompat = true;
|
|
}
|
|
}
|
|
|
|
// INSERT:
|
|
if (queryType == QueryType.insert && jsonparams != null) {
|
|
needRawQuery = false;
|
|
|
|
SQLiteStatement myStatement = mydb.compileStatement(query);
|
|
|
|
bindArgsToStatement(myStatement, jsonparams[i]);
|
|
|
|
long insertId = -1; // (invalid)
|
|
|
|
try {
|
|
insertId = myStatement.executeInsert();
|
|
|
|
// statement has finished with no constraint violation:
|
|
queryResult = new JSONObject();
|
|
if (insertId != -1) {
|
|
queryResult.put("insertId", insertId);
|
|
queryResult.put("rowsAffected", 1);
|
|
} else {
|
|
queryResult.put("rowsAffected", 0);
|
|
}
|
|
} catch (SQLiteException ex) {
|
|
// report error result with the error message
|
|
// could be constraint violation or some other error
|
|
ex.printStackTrace();
|
|
errorMessage = ex.getMessage();
|
|
Log.v("executeSqlBatch", "SQLiteDatabase.executeInsert(): Error=" + errorMessage);
|
|
}
|
|
}
|
|
|
|
if (queryType == QueryType.begin) {
|
|
needRawQuery = false;
|
|
try {
|
|
mydb.beginTransaction();
|
|
|
|
queryResult = new JSONObject();
|
|
queryResult.put("rowsAffected", 0);
|
|
} catch (SQLiteException ex) {
|
|
ex.printStackTrace();
|
|
errorMessage = ex.getMessage();
|
|
Log.v("executeSqlBatch", "SQLiteDatabase.beginTransaction(): Error=" + errorMessage);
|
|
}
|
|
}
|
|
|
|
if (queryType == QueryType.commit) {
|
|
needRawQuery = false;
|
|
try {
|
|
mydb.setTransactionSuccessful();
|
|
mydb.endTransaction();
|
|
|
|
queryResult = new JSONObject();
|
|
queryResult.put("rowsAffected", 0);
|
|
} catch (SQLiteException ex) {
|
|
ex.printStackTrace();
|
|
errorMessage = ex.getMessage();
|
|
Log.v("executeSqlBatch", "SQLiteDatabase.setTransactionSuccessful/endTransaction(): Error=" + errorMessage);
|
|
}
|
|
}
|
|
|
|
if (queryType == QueryType.rollback) {
|
|
needRawQuery = false;
|
|
try {
|
|
mydb.endTransaction();
|
|
|
|
queryResult = new JSONObject();
|
|
queryResult.put("rowsAffected", 0);
|
|
} catch (SQLiteException ex) {
|
|
ex.printStackTrace();
|
|
errorMessage = ex.getMessage();
|
|
Log.v("executeSqlBatch", "SQLiteDatabase.endTransaction(): Error=" + errorMessage);
|
|
}
|
|
}
|
|
|
|
// raw query for other statements:
|
|
if (needRawQuery) {
|
|
queryResult = this.executeSqlStatementQuery(mydb, query, jsonparams[i], cbc);
|
|
|
|
if (needRowsAffectedCompat) {
|
|
queryResult.put("rowsAffected", rowsAffectedCompat);
|
|
}
|
|
}
|
|
} catch (Exception ex) {
|
|
ex.printStackTrace();
|
|
errorMessage = ex.getMessage();
|
|
Log.v("executeSqlBatch", "SQLiteAndroidDatabase.executeSql[Batch](): Error=" + errorMessage);
|
|
}
|
|
|
|
try {
|
|
if (queryResult != null) {
|
|
JSONObject r = new JSONObject();
|
|
r.put("qid", query_id);
|
|
|
|
r.put("type", "success");
|
|
r.put("result", queryResult);
|
|
|
|
batchResults.put(r);
|
|
} else {
|
|
JSONObject r = new JSONObject();
|
|
r.put("qid", query_id);
|
|
r.put("type", "error");
|
|
|
|
JSONObject er = new JSONObject();
|
|
er.put("message", errorMessage);
|
|
r.put("result", er);
|
|
|
|
batchResults.put(r);
|
|
}
|
|
} catch (JSONException ex) {
|
|
ex.printStackTrace();
|
|
Log.v("executeSqlBatch", "SQLiteAndroidDatabase.executeSql[Batch](): Error=" + ex.getMessage());
|
|
// TODO what to do?
|
|
}
|
|
}
|
|
|
|
cbc.success(batchResults);
|
|
}
|
|
|
|
private int countRowsAffectedCompat(QueryType queryType, String query, JSONArray[] jsonparams,
|
|
SQLiteDatabase mydb, int i) throws JSONException {
|
|
// quick and dirty way to calculate the rowsAffected in pre-Honeycomb. just do a SELECT
|
|
// beforehand using the same WHERE clause. might not be perfect, but it's better than nothing
|
|
Matcher whereMatcher = WHERE_CLAUSE.matcher(query);
|
|
|
|
String where = "";
|
|
|
|
int pos = 0;
|
|
while (whereMatcher.find(pos)) {
|
|
where = " WHERE " + whereMatcher.group(1);
|
|
pos = whereMatcher.start(1);
|
|
}
|
|
// WHERE clause may be omitted, and also be sure to find the last one,
|
|
// e.g. for cases where there's a subquery
|
|
|
|
// bindings may be in the update clause, so only take the last n
|
|
int numQuestionMarks = 0;
|
|
for (int j = 0; j < where.length(); j++) {
|
|
if (where.charAt(j) == '?') {
|
|
numQuestionMarks++;
|
|
}
|
|
}
|
|
|
|
JSONArray subParams = null;
|
|
|
|
if (jsonparams != null) {
|
|
// only take the last n of every array of sqlArgs
|
|
JSONArray origArray = jsonparams[i];
|
|
subParams = new JSONArray();
|
|
int startPos = origArray.length() - numQuestionMarks;
|
|
for (int j = startPos; j < origArray.length(); j++) {
|
|
subParams.put(j - startPos, origArray.get(j));
|
|
}
|
|
}
|
|
|
|
if (queryType == QueryType.update) {
|
|
Matcher tableMatcher = UPDATE_TABLE_NAME.matcher(query);
|
|
if (tableMatcher.find()) {
|
|
String table = tableMatcher.group(1);
|
|
try {
|
|
SQLiteStatement statement = mydb.compileStatement(
|
|
"SELECT count(*) FROM " + table + where);
|
|
|
|
if (subParams != null) {
|
|
bindArgsToStatement(statement, subParams);
|
|
}
|
|
|
|
return (int)statement.simpleQueryForLong();
|
|
} catch (Exception e) {
|
|
// assume we couldn't count for whatever reason, keep going
|
|
Log.e(SQLiteAndroidDatabase.class.getSimpleName(), "uncaught", e);
|
|
}
|
|
}
|
|
} else { // delete
|
|
Matcher tableMatcher = DELETE_TABLE_NAME.matcher(query);
|
|
if (tableMatcher.find()) {
|
|
String table = tableMatcher.group(1);
|
|
try {
|
|
SQLiteStatement statement = mydb.compileStatement(
|
|
"SELECT count(*) FROM " + table + where);
|
|
bindArgsToStatement(statement, subParams);
|
|
|
|
return (int)statement.simpleQueryForLong();
|
|
} catch (Exception e) {
|
|
// assume we couldn't count for whatever reason, keep going
|
|
Log.e(SQLiteAndroidDatabase.class.getSimpleName(), "uncaught", e);
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private void bindArgsToStatement(SQLiteStatement myStatement, JSONArray sqlArgs) throws JSONException {
|
|
for (int i = 0; i < sqlArgs.length(); i++) {
|
|
if (sqlArgs.get(i) instanceof Float || sqlArgs.get(i) instanceof Double) {
|
|
myStatement.bindDouble(i + 1, sqlArgs.getDouble(i));
|
|
} else if (sqlArgs.get(i) instanceof Number) {
|
|
myStatement.bindLong(i + 1, sqlArgs.getLong(i));
|
|
} else if (sqlArgs.isNull(i)) {
|
|
myStatement.bindNull(i + 1);
|
|
} else {
|
|
myStatement.bindString(i + 1, sqlArgs.getString(i));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get rows results from query cursor.
|
|
*
|
|
* @param cur Cursor into query results
|
|
* @return results in string form
|
|
*/
|
|
private JSONObject executeSqlStatementQuery(SQLiteDatabase mydb,
|
|
String query, JSONArray paramsAsJson,
|
|
CallbackContext cbc) throws Exception {
|
|
JSONObject rowsResult = new JSONObject();
|
|
|
|
Cursor cur = null;
|
|
try {
|
|
String[] params = null;
|
|
|
|
params = new String[paramsAsJson.length()];
|
|
|
|
for (int j = 0; j < paramsAsJson.length(); j++) {
|
|
if (paramsAsJson.isNull(j))
|
|
params[j] = "";
|
|
else
|
|
params[j] = paramsAsJson.getString(j);
|
|
}
|
|
|
|
cur = mydb.rawQuery(query, params);
|
|
} catch (Exception ex) {
|
|
ex.printStackTrace();
|
|
String errorMessage = ex.getMessage();
|
|
Log.v("executeSqlBatch", "SQLiteAndroidDatabase.executeSql[Batch](): Error=" + errorMessage);
|
|
throw ex;
|
|
}
|
|
|
|
// If query result has rows
|
|
if (cur != null && cur.moveToFirst()) {
|
|
JSONArray rowsArrayResult = new JSONArray();
|
|
String key = "";
|
|
int colCount = cur.getColumnCount();
|
|
|
|
// Build up JSON result object for each row
|
|
do {
|
|
JSONObject row = new JSONObject();
|
|
try {
|
|
for (int i = 0; i < colCount; ++i) {
|
|
key = cur.getColumnName(i);
|
|
|
|
if (android.os.Build.VERSION.SDK_INT >= 11) {
|
|
|
|
// Use try & catch just in case android.os.Build.VERSION.SDK_INT >= 11 is lying:
|
|
try {
|
|
bindPostHoneycomb(row, key, cur, i);
|
|
} catch (Exception ex) {
|
|
bindPreHoneycomb(row, key, cur, i);
|
|
}
|
|
} else {
|
|
bindPreHoneycomb(row, key, cur, i);
|
|
}
|
|
}
|
|
|
|
rowsArrayResult.put(row);
|
|
|
|
} catch (JSONException e) {
|
|
e.printStackTrace();
|
|
}
|
|
} while (cur.moveToNext());
|
|
|
|
try {
|
|
rowsResult.put("rows", rowsArrayResult);
|
|
} catch (JSONException e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
if (cur != null) {
|
|
cur.close();
|
|
}
|
|
|
|
return rowsResult;
|
|
}
|
|
|
|
@SuppressLint("NewApi")
|
|
private void bindPostHoneycomb(JSONObject row, String key, Cursor cur, int i) throws JSONException {
|
|
int curType = cur.getType(i);
|
|
|
|
switch (curType) {
|
|
case Cursor.FIELD_TYPE_NULL:
|
|
row.put(key, JSONObject.NULL);
|
|
break;
|
|
case Cursor.FIELD_TYPE_INTEGER:
|
|
row.put(key, cur.getLong(i));
|
|
break;
|
|
case Cursor.FIELD_TYPE_FLOAT:
|
|
row.put(key, cur.getDouble(i));
|
|
break;
|
|
case Cursor.FIELD_TYPE_BLOB:
|
|
row.put(key, new String(Base64.encode(cur.getBlob(i), Base64.DEFAULT)));
|
|
break;
|
|
case Cursor.FIELD_TYPE_STRING:
|
|
default: /* (not expected) */
|
|
row.put(key, cur.getString(i));
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void bindPreHoneycomb(JSONObject row, String key, Cursor cursor, int i) throws JSONException {
|
|
// Since cursor.getType() is not available pre-honeycomb, this is
|
|
// a workaround so we don't have to bind everything as a string
|
|
// Details here: http://stackoverflow.com/q/11658239
|
|
SQLiteCursor sqLiteCursor = (SQLiteCursor) cursor;
|
|
CursorWindow cursorWindow = sqLiteCursor.getWindow();
|
|
int pos = cursor.getPosition();
|
|
if (cursorWindow.isNull(pos, i)) {
|
|
row.put(key, JSONObject.NULL);
|
|
} else if (cursorWindow.isLong(pos, i)) {
|
|
row.put(key, cursor.getLong(i));
|
|
} else if (cursorWindow.isFloat(pos, i)) {
|
|
row.put(key, cursor.getDouble(i));
|
|
} else if (cursorWindow.isBlob(pos, i)) {
|
|
row.put(key, new String(Base64.encode(cursor.getBlob(i), Base64.DEFAULT)));
|
|
} else { // string
|
|
row.put(key, cursor.getString(i));
|
|
}
|
|
}
|
|
|
|
static QueryType getQueryType(String query) {
|
|
Matcher matcher = FIRST_WORD.matcher(query);
|
|
if (matcher.find()) {
|
|
try {
|
|
return QueryType.valueOf(matcher.group(1).toLowerCase());
|
|
} catch (IllegalArgumentException ignore) {
|
|
// unknown verb
|
|
}
|
|
}
|
|
return QueryType.other;
|
|
}
|
|
|
|
static enum QueryType {
|
|
update,
|
|
insert,
|
|
delete,
|
|
select,
|
|
begin,
|
|
commit,
|
|
rollback,
|
|
other
|
|
}
|
|
} /* vim: set expandtab : */
|