Add missing cordova-sqlite-storage plugin resolves issue #84
This commit is contained in:
parent
7c044cf2b8
commit
9a02daa626
11 changed files with 1346 additions and 5 deletions
|
@ -99,4 +99,5 @@
|
||||||
</plugin>
|
</plugin>
|
||||||
<plugin name="cordova-plugin-filepath" spec="^1.3.0" />
|
<plugin name="cordova-plugin-filepath" spec="^1.3.0" />
|
||||||
<engine name="android" spec="7.1.0" />
|
<engine name="android" spec="7.1.0" />
|
||||||
|
<plugin name="cordova-sqlite-storage" spec="2.5.1" />
|
||||||
</widget>
|
</widget>
|
||||||
|
|
15
package-lock.json
generated
15
package-lock.json
generated
|
@ -2413,6 +2413,19 @@
|
||||||
"resolved": "https://registry.npmjs.org/cordova-plugin-whitelist/-/cordova-plugin-whitelist-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/cordova-plugin-whitelist/-/cordova-plugin-whitelist-1.3.3.tgz",
|
||||||
"integrity": "sha1-tehezbv+Wu3tQKG/TuI3LmfZb7Q="
|
"integrity": "sha1-tehezbv+Wu3tQKG/TuI3LmfZb7Q="
|
||||||
},
|
},
|
||||||
|
"cordova-sqlite-storage": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cordova-sqlite-storage/-/cordova-sqlite-storage-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-RMZcheSs9ihxcXUEmcAg8inG0UHZ5rQKQZqLY40jFES8rpiH5/sYeqaTmnuATx5w2apGB7fFLQHWMG2qaxAVtw==",
|
||||||
|
"requires": {
|
||||||
|
"cordova-sqlite-storage-dependencies": "1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cordova-sqlite-storage-dependencies": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cordova-sqlite-storage-dependencies/-/cordova-sqlite-storage-dependencies-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-lJl5uJFrCWrCYYGhpSEXe6sepjgOOzbeh8fFur3LqaSRvx+xFNYtfMYumE0+xqZwSmPODzex+y6I7ixwcBn73Q=="
|
||||||
|
},
|
||||||
"core-js": {
|
"core-js": {
|
||||||
"version": "2.5.5",
|
"version": "2.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.5.tgz",
|
||||||
|
@ -4601,7 +4614,7 @@
|
||||||
},
|
},
|
||||||
"ionic": {
|
"ionic": {
|
||||||
"version": "3.20.0",
|
"version": "3.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/ionic/-/ionic-3.20.0.tgz",
|
"resolved": "http://registry.npmjs.org/ionic/-/ionic-3.20.0.tgz",
|
||||||
"integrity": "sha512-yeLPusYOSyF+VmO+Hf2a5kf2Kx4ST1f3MILM8g+9ckF/MdaoD9UzXif2/sumGem6I6RTrqo9horBmC7QJYcClA==",
|
"integrity": "sha512-yeLPusYOSyF+VmO+Hf2a5kf2Kx4ST1f3MILM8g+9ckF/MdaoD9UzXif2/sumGem6I6RTrqo9horBmC7QJYcClA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
|
|
@ -42,6 +42,7 @@
|
||||||
"cordova-plugin-splashscreen": "^5.0.2",
|
"cordova-plugin-splashscreen": "^5.0.2",
|
||||||
"cordova-plugin-telerik-imagepicker": "^2.1.8",
|
"cordova-plugin-telerik-imagepicker": "^2.1.8",
|
||||||
"cordova-plugin-whitelist": "^1.3.3",
|
"cordova-plugin-whitelist": "^1.3.3",
|
||||||
|
"cordova-sqlite-storage": "2.5.1",
|
||||||
"ionic-angular": "3.9.2",
|
"ionic-angular": "3.9.2",
|
||||||
"ionicons": "4.2.4",
|
"ionicons": "4.2.4",
|
||||||
"moment": "^2.18.1",
|
"moment": "^2.18.1",
|
||||||
|
@ -72,7 +73,8 @@
|
||||||
"cordova-android-support-gradle-release": {
|
"cordova-android-support-gradle-release": {
|
||||||
"ANDROID_SUPPORT_VERSION": "27.+"
|
"ANDROID_SUPPORT_VERSION": "27.+"
|
||||||
},
|
},
|
||||||
"cordova-plugin-filepath": {}
|
"cordova-plugin-filepath": {},
|
||||||
|
"cordova-sqlite-storage": {}
|
||||||
},
|
},
|
||||||
"platforms": [
|
"platforms": [
|
||||||
"android"
|
"android"
|
||||||
|
|
|
@ -49,6 +49,10 @@
|
||||||
{
|
{
|
||||||
"xml": "<feature name=\"FilePath\"><param name=\"android-package\" value=\"com.hiddentao.cordova.filepath.FilePath\" /><param name=\"onload\" value=\"true\" /></feature>",
|
"xml": "<feature name=\"FilePath\"><param name=\"android-package\" value=\"com.hiddentao.cordova.filepath.FilePath\" /><param name=\"onload\" value=\"true\" /></feature>",
|
||||||
"count": 1
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"xml": "<feature name=\"SQLitePlugin\"><param name=\"android-package\" value=\"io.sqlc.SQLitePlugin\" /></feature>",
|
||||||
|
"count": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -135,6 +139,9 @@
|
||||||
},
|
},
|
||||||
"cordova-plugin-filepath": {
|
"cordova-plugin-filepath": {
|
||||||
"PACKAGE_NAME": "com.monkeystew.goober_m"
|
"PACKAGE_NAME": "com.monkeystew.goober_m"
|
||||||
|
},
|
||||||
|
"cordova-sqlite-storage": {
|
||||||
|
"PACKAGE_NAME": "com.monkeystew.goober_m"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependent_plugins": {},
|
"dependent_plugins": {},
|
||||||
|
@ -384,6 +391,14 @@
|
||||||
"clobbers": [
|
"clobbers": [
|
||||||
"window.FilePath"
|
"window.FilePath"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cordova-sqlite-storage.SQLitePlugin",
|
||||||
|
"file": "plugins/cordova-sqlite-storage/www/SQLitePlugin.js",
|
||||||
|
"pluginId": "cordova-sqlite-storage",
|
||||||
|
"clobbers": [
|
||||||
|
"SQLitePlugin"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"plugin_metadata": {
|
"plugin_metadata": {
|
||||||
|
@ -397,6 +412,7 @@
|
||||||
"cordova-plugin-splashscreen": "5.0.2",
|
"cordova-plugin-splashscreen": "5.0.2",
|
||||||
"cordova-plugin-device": "2.0.2",
|
"cordova-plugin-device": "2.0.2",
|
||||||
"cordova-android-support-gradle-release": "1.4.4",
|
"cordova-android-support-gradle-release": "1.4.4",
|
||||||
"cordova-plugin-filepath": "1.4.2"
|
"cordova-plugin-filepath": "1.4.2",
|
||||||
|
"cordova-sqlite-storage": "2.5.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
BIN
platforms/android/app/libs/sqlite-connector.jar
Normal file
BIN
platforms/android/app/libs/sqlite-connector.jar
Normal file
Binary file not shown.
BIN
platforms/android/app/libs/sqlite-native-driver.jar
Normal file
BIN
platforms/android/app/libs/sqlite-native-driver.jar
Normal file
Binary file not shown.
|
@ -0,0 +1,577 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2012-present Christopher J. Brody (aka Chris Brody)
|
||||||
|
* Copyright (c) 2005-2010, Nitobi Software Inc.
|
||||||
|
* Copyright (c) 2010, IBM Corporation
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.sqlc;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.CursorWindow;
|
||||||
|
|
||||||
|
import android.database.sqlite.SQLiteConstraintException;
|
||||||
|
import android.database.sqlite.SQLiteCursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteException;
|
||||||
|
import android.database.sqlite.SQLiteStatement;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import java.lang.IllegalArgumentException;
|
||||||
|
import java.lang.Number;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
private static final boolean isPostHoneycomb = android.os.Build.VERSION.SDK_INT >= 11;
|
||||||
|
|
||||||
|
File dbFile;
|
||||||
|
|
||||||
|
SQLiteDatabase mydb;
|
||||||
|
|
||||||
|
boolean isTransactionActive = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
if (isTransactionActive) {
|
||||||
|
mydb.endTransaction();
|
||||||
|
isTransactionActive = false;
|
||||||
|
}
|
||||||
|
mydb.close();
|
||||||
|
mydb = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void bugWorkaround() throws Exception {
|
||||||
|
this.closeDatabaseNow();
|
||||||
|
this.open(dbFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a batch request and sends the results via cbc.
|
||||||
|
*
|
||||||
|
* @param queryarr Array of query strings
|
||||||
|
* @param jsonparamsArr Array of JSON query parameters
|
||||||
|
* @param cbc Callback context from Cordova API
|
||||||
|
*/
|
||||||
|
void executeSqlBatch(String[] queryarr, JSONArray[] jsonparamsArr, 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
|
||||||
|
// (internal plugin error)
|
||||||
|
cbc.error("INTERNAL PLUGIN ERROR: database not open");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int len = queryarr.length;
|
||||||
|
JSONArray batchResults = new JSONArray();
|
||||||
|
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
executeSqlBatchStatement(queryarr[i], jsonparamsArr[i], batchResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
cbc.success(batchResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
private void executeSqlBatchStatement(String query, JSONArray json_params, JSONArray batchResults) {
|
||||||
|
|
||||||
|
if (mydb == null) {
|
||||||
|
// Should not happen here
|
||||||
|
return;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
int rowsAffectedCompat = 0;
|
||||||
|
boolean needRowsAffectedCompat = false;
|
||||||
|
|
||||||
|
JSONObject queryResult = null;
|
||||||
|
|
||||||
|
String errorMessage = "unknown";
|
||||||
|
int code = 0; // SQLException.UNKNOWN_ERR
|
||||||
|
|
||||||
|
try {
|
||||||
|
boolean needRawQuery = true;
|
||||||
|
|
||||||
|
//Log.v("executeSqlBatch", "get query type");
|
||||||
|
QueryType queryType = getQueryType(query);
|
||||||
|
//Log.v("executeSqlBatch", "query type: " + queryType);
|
||||||
|
|
||||||
|
if (queryType == QueryType.update || queryType == queryType.delete) {
|
||||||
|
if (isPostHoneycomb) {
|
||||||
|
SQLiteStatement myStatement = mydb.compileStatement(query);
|
||||||
|
|
||||||
|
if (json_params != null) {
|
||||||
|
bindArgsToStatement(myStatement, json_params);
|
||||||
|
}
|
||||||
|
|
||||||
|
int rowsAffected = -1; // (assuming invalid)
|
||||||
|
|
||||||
|
// Use try & catch just in case android.os.Build.VERSION.SDK_INT >= 11 is lying:
|
||||||
|
// (Catch SQLiteException here to avoid extra retry)
|
||||||
|
try {
|
||||||
|
rowsAffected = myStatement.executeUpdateDelete();
|
||||||
|
// Indicate valid results:
|
||||||
|
needRawQuery = false;
|
||||||
|
} catch (SQLiteConstraintException ex) {
|
||||||
|
// Indicate problem & stop this query:
|
||||||
|
ex.printStackTrace();
|
||||||
|
errorMessage = "constraint failure: " + ex.getMessage();
|
||||||
|
code = 6; // SQLException.CONSTRAINT_ERR
|
||||||
|
Log.v("executeSqlBatch", "SQLiteStatement.executeUpdateDelete(): Error=" + errorMessage);
|
||||||
|
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.
|
||||||
|
ex.printStackTrace();
|
||||||
|
Log.v("executeSqlBatch", "SQLiteStatement.executeUpdateDelete(): runtime error (fallback to old API): " + errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// "finally" cleanup myStatement
|
||||||
|
myStatement.close();
|
||||||
|
|
||||||
|
if (rowsAffected != -1) {
|
||||||
|
queryResult = new JSONObject();
|
||||||
|
queryResult.put("rowsAffected", rowsAffected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needRawQuery) { // for pre-honeycomb behavior
|
||||||
|
rowsAffectedCompat = countRowsAffectedCompat(queryType, query, json_params, mydb);
|
||||||
|
needRowsAffectedCompat = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSERT:
|
||||||
|
if (queryType == QueryType.insert && json_params != null) {
|
||||||
|
needRawQuery = false;
|
||||||
|
|
||||||
|
SQLiteStatement myStatement = mydb.compileStatement(query);
|
||||||
|
|
||||||
|
bindArgsToStatement(myStatement, json_params);
|
||||||
|
|
||||||
|
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 (SQLiteConstraintException ex) {
|
||||||
|
// report constraint violation error result with the error message
|
||||||
|
ex.printStackTrace();
|
||||||
|
errorMessage = "constraint failure: " + ex.getMessage();
|
||||||
|
code = 6; // SQLException.CONSTRAINT_ERR
|
||||||
|
Log.v("executeSqlBatch", "SQLiteDatabase.executeInsert(): Error=" + errorMessage);
|
||||||
|
} catch (SQLiteException ex) {
|
||||||
|
// report some other error result with the error message
|
||||||
|
ex.printStackTrace();
|
||||||
|
errorMessage = ex.getMessage();
|
||||||
|
Log.v("executeSqlBatch", "SQLiteDatabase.executeInsert(): Error=" + errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// "finally" cleanup myStatement
|
||||||
|
myStatement.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryType == QueryType.begin) {
|
||||||
|
needRawQuery = false;
|
||||||
|
try {
|
||||||
|
mydb.beginTransaction();
|
||||||
|
isTransactionActive = true;
|
||||||
|
|
||||||
|
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();
|
||||||
|
isTransactionActive = false;
|
||||||
|
|
||||||
|
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();
|
||||||
|
isTransactionActive = false;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
try {
|
||||||
|
queryResult = this.executeSqlStatementQuery(mydb, query, json_params);
|
||||||
|
|
||||||
|
} catch (SQLiteConstraintException ex) {
|
||||||
|
// report constraint violation error result with the error message
|
||||||
|
ex.printStackTrace();
|
||||||
|
errorMessage = "constraint failure: " + ex.getMessage();
|
||||||
|
code = 6; // SQLException.CONSTRAINT_ERR
|
||||||
|
Log.v("executeSqlBatch", "Raw query error=" + errorMessage);
|
||||||
|
} catch (SQLiteException ex) {
|
||||||
|
// report some other error result with the error message
|
||||||
|
ex.printStackTrace();
|
||||||
|
errorMessage = ex.getMessage();
|
||||||
|
Log.v("executeSqlBatch", "Raw query error=" + errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
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("type", "success");
|
||||||
|
r.put("result", queryResult);
|
||||||
|
|
||||||
|
batchResults.put(r);
|
||||||
|
} else {
|
||||||
|
JSONObject r = new JSONObject();
|
||||||
|
r.put("type", "error");
|
||||||
|
|
||||||
|
JSONObject er = new JSONObject();
|
||||||
|
er.put("message", errorMessage);
|
||||||
|
er.put("code", code);
|
||||||
|
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?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final int countRowsAffectedCompat(QueryType queryType, String query, JSONArray json_params,
|
||||||
|
SQLiteDatabase mydb) 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 (json_params != null) {
|
||||||
|
// only take the last n of every array of sqlArgs
|
||||||
|
JSONArray origArray = json_params;
|
||||||
|
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) 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 (isPostHoneycomb) {
|
||||||
|
|
||||||
|
// 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_STRING:
|
||||||
|
default: /* (BLOB) */
|
||||||
|
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 {
|
||||||
|
// STRING or BLOB:
|
||||||
|
row.put(key, cursor.getString(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static QueryType getQueryType(String query) {
|
||||||
|
Matcher matcher = FIRST_WORD.matcher(query);
|
||||||
|
|
||||||
|
// FIND & return query type, or throw:
|
||||||
|
if (matcher.find()) {
|
||||||
|
try {
|
||||||
|
String first = matcher.group(1);
|
||||||
|
|
||||||
|
// explictly reject if blank
|
||||||
|
// (needed for SQLCipher version)
|
||||||
|
if (first.length() == 0) throw new RuntimeException("query not found");
|
||||||
|
|
||||||
|
return QueryType.valueOf(first.toLowerCase(Locale.ENGLISH));
|
||||||
|
} catch (IllegalArgumentException ignore) {
|
||||||
|
// unknown verb (NOT blank)
|
||||||
|
return QueryType.other;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// explictly reject if blank
|
||||||
|
// (needed for SQLCipher version)
|
||||||
|
throw new RuntimeException("query not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static enum QueryType {
|
||||||
|
update,
|
||||||
|
insert,
|
||||||
|
delete,
|
||||||
|
select,
|
||||||
|
begin,
|
||||||
|
commit,
|
||||||
|
rollback,
|
||||||
|
other
|
||||||
|
}
|
||||||
|
} /* vim: set expandtab : */
|
|
@ -0,0 +1,286 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2012-present Christopher J. Brody (aka Chris Brody)
|
||||||
|
* Copyright (c) 2005-2010, Nitobi Software Inc.
|
||||||
|
* Copyright (c) 2010, IBM Corporation
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.sqlc;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import java.lang.IllegalArgumentException;
|
||||||
|
import java.lang.Number;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
import io.liteglue.SQLCode;
|
||||||
|
import io.liteglue.SQLColumnType;
|
||||||
|
import io.liteglue.SQLiteConnector;
|
||||||
|
import io.liteglue.SQLiteConnection;
|
||||||
|
import io.liteglue.SQLiteOpenFlags;
|
||||||
|
import io.liteglue.SQLiteStatement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android SQLite-Connector Database helper class
|
||||||
|
*/
|
||||||
|
class SQLiteConnectorDatabase extends SQLiteAndroidDatabase
|
||||||
|
{
|
||||||
|
static SQLiteConnector connector = new SQLiteConnector();
|
||||||
|
|
||||||
|
SQLiteConnection mydb;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOTE: Using default constructor, no explicit constructor.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a database.
|
||||||
|
*
|
||||||
|
* @param dbFile The database File specification
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
void open(File dbFile) throws Exception {
|
||||||
|
mydb = connector.newSQLiteConnection(dbFile.getAbsolutePath(),
|
||||||
|
SQLiteOpenFlags.READWRITE | SQLiteOpenFlags.CREATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close a database (in the current thread).
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
void closeDatabaseNow() {
|
||||||
|
try {
|
||||||
|
if (mydb != null)
|
||||||
|
mydb.dispose();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(SQLitePlugin.class.getSimpleName(), "couldn't close database, ignoring", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ignore Android bug workaround for NDK version
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
void bugWorkaround() { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 cbc Callback context from Cordova API
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
void executeSqlBatch( String[] queryarr, JSONArray[] jsonparams, 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
int len = queryarr.length;
|
||||||
|
JSONArray batchResults = new JSONArray();
|
||||||
|
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
int rowsAffectedCompat = 0;
|
||||||
|
boolean needRowsAffectedCompat = false;
|
||||||
|
|
||||||
|
JSONObject queryResult = null;
|
||||||
|
|
||||||
|
String errorMessage = "unknown";
|
||||||
|
int sqliteErrorCode = -1;
|
||||||
|
int code = 0; // SQLException.UNKNOWN_ERR
|
||||||
|
|
||||||
|
try {
|
||||||
|
String query = queryarr[i];
|
||||||
|
|
||||||
|
long lastTotal = mydb.getTotalChanges();
|
||||||
|
queryResult = this.executeSQLiteStatement(query, jsonparams[i], cbc);
|
||||||
|
long newTotal = mydb.getTotalChanges();
|
||||||
|
long rowsAffected = newTotal - lastTotal;
|
||||||
|
|
||||||
|
queryResult.put("rowsAffected", rowsAffected);
|
||||||
|
if (rowsAffected > 0) {
|
||||||
|
long insertId = mydb.getLastInsertRowid();
|
||||||
|
if (insertId > 0) {
|
||||||
|
queryResult.put("insertId", insertId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
sqliteErrorCode = ex.getErrorCode();
|
||||||
|
errorMessage = ex.getMessage();
|
||||||
|
Log.v("executeSqlBatch", "SQLitePlugin.executeSql[Batch](): SQL Error code = " + sqliteErrorCode + " message = " + errorMessage);
|
||||||
|
|
||||||
|
switch(sqliteErrorCode) {
|
||||||
|
case SQLCode.ERROR:
|
||||||
|
code = 5; // SQLException.SYNTAX_ERR
|
||||||
|
break;
|
||||||
|
case 13: // SQLITE_FULL
|
||||||
|
code = 4; // SQLException.QUOTA_ERR
|
||||||
|
break;
|
||||||
|
case SQLCode.CONSTRAINT:
|
||||||
|
code = 6; // SQLException.CONSTRAINT_ERR
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
/* do nothing */
|
||||||
|
}
|
||||||
|
} catch (JSONException ex) {
|
||||||
|
// NOT expected:
|
||||||
|
ex.printStackTrace();
|
||||||
|
errorMessage = ex.getMessage();
|
||||||
|
code = 0; // SQLException.UNKNOWN_ERR
|
||||||
|
Log.e("executeSqlBatch", "SQLitePlugin.executeSql[Batch](): UNEXPECTED JSON Error=" + errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (queryResult != null) {
|
||||||
|
JSONObject r = new JSONObject();
|
||||||
|
|
||||||
|
r.put("type", "success");
|
||||||
|
r.put("result", queryResult);
|
||||||
|
|
||||||
|
batchResults.put(r);
|
||||||
|
} else {
|
||||||
|
JSONObject r = new JSONObject();
|
||||||
|
r.put("type", "error");
|
||||||
|
|
||||||
|
JSONObject er = new JSONObject();
|
||||||
|
er.put("message", errorMessage);
|
||||||
|
er.put("code", code);
|
||||||
|
r.put("result", er);
|
||||||
|
|
||||||
|
batchResults.put(r);
|
||||||
|
}
|
||||||
|
} catch (JSONException ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
Log.e("executeSqlBatch", "SQLitePlugin.executeSql[Batch](): Error=" + ex.getMessage());
|
||||||
|
// TODO what to do?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cbc.success(batchResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get rows results from query cursor.
|
||||||
|
*
|
||||||
|
* @param cur Cursor into query results
|
||||||
|
* @return results in string form
|
||||||
|
*/
|
||||||
|
private JSONObject executeSQLiteStatement(String query, JSONArray paramsAsJson,
|
||||||
|
CallbackContext cbc) throws JSONException, SQLException {
|
||||||
|
JSONObject rowsResult = new JSONObject();
|
||||||
|
|
||||||
|
boolean hasRows = false;
|
||||||
|
|
||||||
|
SQLiteStatement myStatement = mydb.prepareStatement(query);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String[] params = null;
|
||||||
|
|
||||||
|
params = new String[paramsAsJson.length()];
|
||||||
|
|
||||||
|
for (int i = 0; i < paramsAsJson.length(); ++i) {
|
||||||
|
if (paramsAsJson.isNull(i)) {
|
||||||
|
myStatement.bindNull(i + 1);
|
||||||
|
} else {
|
||||||
|
Object p = paramsAsJson.get(i);
|
||||||
|
if (p instanceof Float || p instanceof Double)
|
||||||
|
myStatement.bindDouble(i + 1, paramsAsJson.getDouble(i));
|
||||||
|
else if (p instanceof Number)
|
||||||
|
myStatement.bindLong(i + 1, paramsAsJson.getLong(i));
|
||||||
|
else
|
||||||
|
myStatement.bindTextNativeString(i + 1, paramsAsJson.getString(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasRows = myStatement.step();
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
String errorMessage = ex.getMessage();
|
||||||
|
Log.v("executeSqlBatch", "SQLitePlugin.executeSql[Batch](): Error=" + errorMessage);
|
||||||
|
|
||||||
|
// cleanup statement and throw the exception:
|
||||||
|
myStatement.dispose();
|
||||||
|
throw ex;
|
||||||
|
} catch (JSONException ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
String errorMessage = ex.getMessage();
|
||||||
|
Log.v("executeSqlBatch", "SQLitePlugin.executeSql[Batch](): Error=" + errorMessage);
|
||||||
|
|
||||||
|
// cleanup statement and throw the exception:
|
||||||
|
myStatement.dispose();
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If query result has rows
|
||||||
|
if (hasRows) {
|
||||||
|
JSONArray rowsArrayResult = new JSONArray();
|
||||||
|
String key = "";
|
||||||
|
int colCount = myStatement.getColumnCount();
|
||||||
|
|
||||||
|
// Build up JSON result object for each row
|
||||||
|
do {
|
||||||
|
JSONObject row = new JSONObject();
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < colCount; ++i) {
|
||||||
|
key = myStatement.getColumnName(i);
|
||||||
|
|
||||||
|
switch (myStatement.getColumnType(i)) {
|
||||||
|
case SQLColumnType.NULL:
|
||||||
|
row.put(key, JSONObject.NULL);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SQLColumnType.REAL:
|
||||||
|
row.put(key, myStatement.getColumnDouble(i));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SQLColumnType.INTEGER:
|
||||||
|
row.put(key, myStatement.getColumnLong(i));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SQLColumnType.BLOB:
|
||||||
|
case SQLColumnType.TEXT:
|
||||||
|
default: // (just in case)
|
||||||
|
row.put(key, myStatement.getColumnTextNativeString(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsArrayResult.put(row);
|
||||||
|
|
||||||
|
} catch (JSONException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
} while (myStatement.step());
|
||||||
|
|
||||||
|
try {
|
||||||
|
rowsResult.put("rows", rowsArrayResult);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
myStatement.dispose();
|
||||||
|
|
||||||
|
return rowsResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
} /* vim: set expandtab : */
|
433
platforms/android/app/src/main/java/io/sqlc/SQLitePlugin.java
Executable file
433
platforms/android/app/src/main/java/io/sqlc/SQLitePlugin.java
Executable file
|
@ -0,0 +1,433 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2012-present Christopher J. Brody (aka Chris Brody)
|
||||||
|
* Copyright (c) 2005-2010, Nitobi Software Inc.
|
||||||
|
* Copyright (c) 2010, IBM Corporation
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.sqlc;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import java.lang.IllegalArgumentException;
|
||||||
|
import java.lang.Number;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
|
import org.apache.cordova.CallbackContext;
|
||||||
|
import org.apache.cordova.CordovaPlugin;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
public class SQLitePlugin extends CordovaPlugin {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multiple database runner map (static).
|
||||||
|
*
|
||||||
|
* NOTE: no public static accessor to db (runner) map since it is not
|
||||||
|
* expected to work properly with db threading.
|
||||||
|
*
|
||||||
|
* FUTURE TBD put DBRunner into a public class that can provide external accessor.
|
||||||
|
*
|
||||||
|
* ADDITIONAL NOTE: Storing as Map<String, DBRunner> to avoid portabiity issue
|
||||||
|
* between Java 6/7/8 as discussed in:
|
||||||
|
* https://gist.github.com/AlainODea/1375759b8720a3f9f094
|
||||||
|
*
|
||||||
|
* THANKS to @NeoLSN (Jason Yang/楊朝傑) for giving the pointer in:
|
||||||
|
* https://github.com/litehelpers/Cordova-sqlite-storage/issues/727
|
||||||
|
*/
|
||||||
|
static Map<String, DBRunner> dbrmap = new ConcurrentHashMap<String, DBRunner>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOTE: Using default constructor, no explicit constructor.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the request and returns PluginResult.
|
||||||
|
*
|
||||||
|
* @param actionAsString The action to execute.
|
||||||
|
* @param args JSONArry of arguments for the plugin.
|
||||||
|
* @param cbc Callback context from Cordova API
|
||||||
|
* @return Whether the action was valid.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean execute(String actionAsString, JSONArray args, CallbackContext cbc) {
|
||||||
|
|
||||||
|
Action action;
|
||||||
|
try {
|
||||||
|
action = Action.valueOf(actionAsString);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// shouldn't ever happen
|
||||||
|
Log.e(SQLitePlugin.class.getSimpleName(), "unexpected error", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return executeAndPossiblyThrow(action, args, cbc);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
// TODO: signal JSON problem to JS
|
||||||
|
Log.e(SQLitePlugin.class.getSimpleName(), "unexpected error", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean executeAndPossiblyThrow(Action action, JSONArray args, CallbackContext cbc)
|
||||||
|
throws JSONException {
|
||||||
|
|
||||||
|
boolean status = true;
|
||||||
|
JSONObject o;
|
||||||
|
String echo_value;
|
||||||
|
String dbname;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case echoStringValue:
|
||||||
|
o = args.getJSONObject(0);
|
||||||
|
echo_value = o.getString("value");
|
||||||
|
cbc.success(echo_value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case open:
|
||||||
|
o = args.getJSONObject(0);
|
||||||
|
dbname = o.getString("name");
|
||||||
|
// open database and start reading its queue
|
||||||
|
this.startDatabase(dbname, o, cbc);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case close:
|
||||||
|
o = args.getJSONObject(0);
|
||||||
|
dbname = o.getString("path");
|
||||||
|
// put request in the q to close the db
|
||||||
|
this.closeDatabase(dbname, cbc);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case delete:
|
||||||
|
o = args.getJSONObject(0);
|
||||||
|
dbname = o.getString("path");
|
||||||
|
|
||||||
|
deleteDatabase(dbname, cbc);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case executeSqlBatch:
|
||||||
|
case backgroundExecuteSqlBatch:
|
||||||
|
JSONObject allargs = args.getJSONObject(0);
|
||||||
|
JSONObject dbargs = allargs.getJSONObject("dbargs");
|
||||||
|
dbname = dbargs.getString("dbname");
|
||||||
|
JSONArray txargs = allargs.getJSONArray("executes");
|
||||||
|
|
||||||
|
if (txargs.isNull(0)) {
|
||||||
|
cbc.error("INTERNAL PLUGIN ERROR: missing executes list");
|
||||||
|
} else {
|
||||||
|
int len = txargs.length();
|
||||||
|
String[] queries = new String[len];
|
||||||
|
JSONArray[] jsonparams = new JSONArray[len];
|
||||||
|
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
JSONObject a = txargs.getJSONObject(i);
|
||||||
|
queries[i] = a.getString("sql");
|
||||||
|
jsonparams[i] = a.getJSONArray("params");
|
||||||
|
}
|
||||||
|
|
||||||
|
// put db query in the queue to be executed in the db thread:
|
||||||
|
DBQuery q = new DBQuery(queries, jsonparams, cbc);
|
||||||
|
DBRunner r = dbrmap.get(dbname);
|
||||||
|
if (r != null) {
|
||||||
|
try {
|
||||||
|
r.q.put(q);
|
||||||
|
} catch(Exception e) {
|
||||||
|
Log.e(SQLitePlugin.class.getSimpleName(), "couldn't add to queue", e);
|
||||||
|
cbc.error("INTERNAL PLUGIN ERROR: couldn't add to queue");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cbc.error("INTERNAL PLUGIN ERROR: database not open");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up and close all open databases.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
while (!dbrmap.isEmpty()) {
|
||||||
|
String dbname = dbrmap.keySet().iterator().next();
|
||||||
|
|
||||||
|
this.closeDatabaseNow(dbname);
|
||||||
|
|
||||||
|
DBRunner r = dbrmap.get(dbname);
|
||||||
|
try {
|
||||||
|
// stop the db runner thread:
|
||||||
|
r.q.put(new DBQuery());
|
||||||
|
} catch(Exception e) {
|
||||||
|
Log.e(SQLitePlugin.class.getSimpleName(), "INTERNAL PLUGIN CLEANUP ERROR: could not stop db thread due to exception", e);
|
||||||
|
}
|
||||||
|
dbrmap.remove(dbname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// LOCAL METHODS
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private void startDatabase(String dbname, JSONObject options, CallbackContext cbc) {
|
||||||
|
DBRunner r = dbrmap.get(dbname);
|
||||||
|
|
||||||
|
if (r != null) {
|
||||||
|
// NO LONGER EXPECTED due to BUG 666 workaround solution:
|
||||||
|
cbc.error("INTERNAL ERROR: database already open for db name: " + dbname);
|
||||||
|
} else {
|
||||||
|
r = new DBRunner(dbname, options, cbc);
|
||||||
|
dbrmap.put(dbname, r);
|
||||||
|
this.cordova.getThreadPool().execute(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Open a database.
|
||||||
|
*
|
||||||
|
* @param dbName The name of the database file
|
||||||
|
*/
|
||||||
|
private SQLiteAndroidDatabase openDatabase(String dbname, CallbackContext cbc, boolean old_impl) throws Exception {
|
||||||
|
try {
|
||||||
|
// ASSUMPTION: no db (connection/handle) is already stored in the map
|
||||||
|
// [should be true according to the code in DBRunner.run()]
|
||||||
|
|
||||||
|
File dbfile = this.cordova.getActivity().getDatabasePath(dbname);
|
||||||
|
|
||||||
|
if (!dbfile.exists()) {
|
||||||
|
dbfile.getParentFile().mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.v("info", "Open sqlite db: " + dbfile.getAbsolutePath());
|
||||||
|
|
||||||
|
SQLiteAndroidDatabase mydb = old_impl ? new SQLiteAndroidDatabase() : new SQLiteConnectorDatabase();
|
||||||
|
mydb.open(dbfile);
|
||||||
|
|
||||||
|
if (cbc != null) // XXX Android locking/closing BUG workaround
|
||||||
|
cbc.success();
|
||||||
|
|
||||||
|
return mydb;
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (cbc != null) // XXX Android locking/closing BUG workaround
|
||||||
|
cbc.error("can't open database " + e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close a database (in another thread).
|
||||||
|
*
|
||||||
|
* @param dbName The name of the database file
|
||||||
|
*/
|
||||||
|
private void closeDatabase(String dbname, CallbackContext cbc) {
|
||||||
|
DBRunner r = dbrmap.get(dbname);
|
||||||
|
if (r != null) {
|
||||||
|
try {
|
||||||
|
r.q.put(new DBQuery(false, cbc));
|
||||||
|
} catch(Exception e) {
|
||||||
|
if (cbc != null) {
|
||||||
|
cbc.error("couldn't close database" + e);
|
||||||
|
}
|
||||||
|
Log.e(SQLitePlugin.class.getSimpleName(), "couldn't close database", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (cbc != null) {
|
||||||
|
cbc.success();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close a database (in the current thread).
|
||||||
|
*
|
||||||
|
* @param dbname The name of the database file
|
||||||
|
*/
|
||||||
|
private void closeDatabaseNow(String dbname) {
|
||||||
|
DBRunner r = dbrmap.get(dbname);
|
||||||
|
|
||||||
|
if (r != null) {
|
||||||
|
SQLiteAndroidDatabase mydb = r.mydb;
|
||||||
|
|
||||||
|
if (mydb != null)
|
||||||
|
mydb.closeDatabaseNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteDatabase(String dbname, CallbackContext cbc) {
|
||||||
|
DBRunner r = dbrmap.get(dbname);
|
||||||
|
if (r != null) {
|
||||||
|
try {
|
||||||
|
r.q.put(new DBQuery(true, cbc));
|
||||||
|
} catch(Exception e) {
|
||||||
|
if (cbc != null) {
|
||||||
|
cbc.error("couldn't close database" + e);
|
||||||
|
}
|
||||||
|
Log.e(SQLitePlugin.class.getSimpleName(), "couldn't close database", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
boolean deleteResult = this.deleteDatabaseNow(dbname);
|
||||||
|
if (deleteResult) {
|
||||||
|
cbc.success();
|
||||||
|
} else {
|
||||||
|
cbc.error("couldn't delete database");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a database.
|
||||||
|
*
|
||||||
|
* @param dbName The name of the database file
|
||||||
|
*
|
||||||
|
* @return true if successful or false if an exception was encountered
|
||||||
|
*/
|
||||||
|
private boolean deleteDatabaseNow(String dbname) {
|
||||||
|
File dbfile = this.cordova.getActivity().getDatabasePath(dbname);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return cordova.getActivity().deleteDatabase(dbfile.getAbsolutePath());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(SQLitePlugin.class.getSimpleName(), "couldn't delete database", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DBRunner implements Runnable {
|
||||||
|
final String dbname;
|
||||||
|
private boolean oldImpl;
|
||||||
|
private boolean bugWorkaround;
|
||||||
|
|
||||||
|
final BlockingQueue<DBQuery> q;
|
||||||
|
final CallbackContext openCbc;
|
||||||
|
|
||||||
|
SQLiteAndroidDatabase mydb;
|
||||||
|
|
||||||
|
DBRunner(final String dbname, JSONObject options, CallbackContext cbc) {
|
||||||
|
this.dbname = dbname;
|
||||||
|
this.oldImpl = options.has("androidOldDatabaseImplementation");
|
||||||
|
Log.v(SQLitePlugin.class.getSimpleName(), "Android db implementation: built-in android.database.sqlite package");
|
||||||
|
this.bugWorkaround = this.oldImpl && options.has("androidBugWorkaround");
|
||||||
|
if (this.bugWorkaround)
|
||||||
|
Log.v(SQLitePlugin.class.getSimpleName(), "Android db closing/locking workaround applied");
|
||||||
|
|
||||||
|
this.q = new LinkedBlockingQueue<DBQuery>();
|
||||||
|
this.openCbc = cbc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
this.mydb = openDatabase(dbname, this.openCbc, this.oldImpl);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(SQLitePlugin.class.getSimpleName(), "unexpected error, stopping db thread", e);
|
||||||
|
dbrmap.remove(dbname);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DBQuery dbq = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
dbq = q.take();
|
||||||
|
|
||||||
|
while (!dbq.stop) {
|
||||||
|
mydb.executeSqlBatch(dbq.queries, dbq.jsonparams, dbq.cbc);
|
||||||
|
|
||||||
|
if (this.bugWorkaround && dbq.queries.length == 1 && dbq.queries[0] == "COMMIT")
|
||||||
|
mydb.bugWorkaround();
|
||||||
|
|
||||||
|
dbq = q.take();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(SQLitePlugin.class.getSimpleName(), "unexpected error", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dbq != null && dbq.close) {
|
||||||
|
try {
|
||||||
|
closeDatabaseNow(dbname);
|
||||||
|
|
||||||
|
dbrmap.remove(dbname); // (should) remove ourself
|
||||||
|
|
||||||
|
if (!dbq.delete) {
|
||||||
|
dbq.cbc.success();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
boolean deleteResult = deleteDatabaseNow(dbname);
|
||||||
|
if (deleteResult) {
|
||||||
|
dbq.cbc.success();
|
||||||
|
} else {
|
||||||
|
dbq.cbc.error("couldn't delete database");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(SQLitePlugin.class.getSimpleName(), "couldn't delete database", e);
|
||||||
|
dbq.cbc.error("couldn't delete database: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(SQLitePlugin.class.getSimpleName(), "couldn't close database", e);
|
||||||
|
if (dbq.cbc != null) {
|
||||||
|
dbq.cbc.error("couldn't close database: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class DBQuery {
|
||||||
|
// XXX TODO replace with DBRunner action enum:
|
||||||
|
final boolean stop;
|
||||||
|
final boolean close;
|
||||||
|
final boolean delete;
|
||||||
|
final String[] queries;
|
||||||
|
final JSONArray[] jsonparams;
|
||||||
|
final CallbackContext cbc;
|
||||||
|
|
||||||
|
DBQuery(String[] myqueries, JSONArray[] params, CallbackContext c) {
|
||||||
|
this.stop = false;
|
||||||
|
this.close = false;
|
||||||
|
this.delete = false;
|
||||||
|
this.queries = myqueries;
|
||||||
|
this.jsonparams = params;
|
||||||
|
this.cbc = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
DBQuery(boolean delete, CallbackContext cbc) {
|
||||||
|
this.stop = true;
|
||||||
|
this.close = true;
|
||||||
|
this.delete = delete;
|
||||||
|
this.queries = null;
|
||||||
|
this.jsonparams = null;
|
||||||
|
this.cbc = cbc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// signal the DBRunner thread to stop:
|
||||||
|
DBQuery() {
|
||||||
|
this.stop = true;
|
||||||
|
this.close = false;
|
||||||
|
this.delete = false;
|
||||||
|
this.queries = null;
|
||||||
|
this.jsonparams = null;
|
||||||
|
this.cbc = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static enum Action {
|
||||||
|
echoStringValue,
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
delete,
|
||||||
|
executeSqlBatch,
|
||||||
|
backgroundExecuteSqlBatch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set expandtab : */
|
|
@ -33,13 +33,16 @@
|
||||||
<param name="android-package" value="com.hiddentao.cordova.filepath.FilePath" />
|
<param name="android-package" value="com.hiddentao.cordova.filepath.FilePath" />
|
||||||
<param name="onload" value="true" />
|
<param name="onload" value="true" />
|
||||||
</feature>
|
</feature>
|
||||||
|
<feature name="SQLitePlugin">
|
||||||
|
<param name="android-package" value="io.sqlc.SQLitePlugin" />
|
||||||
|
</feature>
|
||||||
<feature name="ShareContentPlugin">
|
<feature name="ShareContentPlugin">
|
||||||
<param name="android-package" value="com.ferdinandsilva.android.ShareContentPlugin" />
|
<param name="android-package" value="com.ferdinandsilva.android.ShareContentPlugin" />
|
||||||
</feature>
|
</feature>
|
||||||
<name>Goober</name>
|
<name>Goober</name>
|
||||||
<description>Goober, a mobile app for pnut.io</description>
|
<description>Goober, a mobile app for pnut.io</description>
|
||||||
<author email="gilag@monkeystew.com" href="https://monkeystew.org">Morgan McMillian</author>
|
<author email="gilag@monkeystew.com" href="https://monkeystew.org">Morgan McMillian</author>
|
||||||
<content src="index.html" />
|
<content original-src="index.html" src="http://192.168.1.72:8100" />
|
||||||
<access origin="*" />
|
<access origin="*" />
|
||||||
<allow-navigation href="http://ionic.local/*" />
|
<allow-navigation href="http://ionic.local/*" />
|
||||||
<allow-intent href="http://*/*" />
|
<allow-intent href="http://*/*" />
|
||||||
|
@ -49,6 +52,7 @@
|
||||||
<allow-intent href="mailto:*" />
|
<allow-intent href="mailto:*" />
|
||||||
<allow-intent href="geo:*" />
|
<allow-intent href="geo:*" />
|
||||||
<allow-navigation href="http://10.0.0.212:8100" />
|
<allow-navigation href="http://10.0.0.212:8100" />
|
||||||
|
<allow-navigation href="http://192.168.1.72:8100" />
|
||||||
<allow-intent href="market:*" />
|
<allow-intent href="market:*" />
|
||||||
<icon density="ldpi" src="resources/android/icon/drawable-ldpi-icon.png" />
|
<icon density="ldpi" src="resources/android/icon/drawable-ldpi-icon.png" />
|
||||||
<icon density="mdpi" src="resources/android/icon/drawable-mdpi-icon.png" />
|
<icon density="mdpi" src="resources/android/icon/drawable-mdpi-icon.png" />
|
||||||
|
|
|
@ -245,6 +245,14 @@ module.exports = [
|
||||||
"clobbers": [
|
"clobbers": [
|
||||||
"window.FilePath"
|
"window.FilePath"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cordova-sqlite-storage.SQLitePlugin",
|
||||||
|
"file": "plugins/cordova-sqlite-storage/www/SQLitePlugin.js",
|
||||||
|
"pluginId": "cordova-sqlite-storage",
|
||||||
|
"clobbers": [
|
||||||
|
"SQLitePlugin"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
module.exports.metadata =
|
module.exports.metadata =
|
||||||
|
@ -260,7 +268,8 @@ module.exports.metadata =
|
||||||
"cordova-plugin-splashscreen": "5.0.2",
|
"cordova-plugin-splashscreen": "5.0.2",
|
||||||
"cordova-plugin-device": "2.0.2",
|
"cordova-plugin-device": "2.0.2",
|
||||||
"cordova-android-support-gradle-release": "1.4.4",
|
"cordova-android-support-gradle-release": "1.4.4",
|
||||||
"cordova-plugin-filepath": "1.4.2"
|
"cordova-plugin-filepath": "1.4.2",
|
||||||
|
"cordova-sqlite-storage": "2.5.1"
|
||||||
};
|
};
|
||||||
// BOTTOM OF METADATA
|
// BOTTOM OF METADATA
|
||||||
});
|
});
|
Reference in a new issue