From 5bf0b6752bb2aadf9690e68e7fdf2ff84071d38b Mon Sep 17 00:00:00 2001 From: Ben Ashton Date: Tue, 2 Mar 2021 20:34:02 -0800 Subject: [PATCH] Initial commit --- .jshintrc | 3 + package.json | 31 +++++ plugin.xml | 30 +++++ src/android/CallbackWrapper.java | 72 ++++++++++++ src/android/DB.java | 132 ++++++++++++++++++++++ src/android/DBAction.java | 9 ++ src/android/DBManager.java | 44 ++++++++ src/android/DBRequest.java | 18 +++ src/android/DBRunner.java | 50 ++++++++ src/android/PawSQLite.java | 67 +++++++++++ src/android/QueryWrapper.java | 188 +++++++++++++++++++++++++++++++ src/log.mjs | 14 +++ src/psql_adapter.mjs | 31 +++++ src/psql_adapter_error.mjs | 28 +++++ webpack.config.js | 17 +++ 15 files changed, 734 insertions(+) create mode 100644 .jshintrc create mode 100755 package.json create mode 100644 plugin.xml create mode 100644 src/android/CallbackWrapper.java create mode 100644 src/android/DB.java create mode 100644 src/android/DBAction.java create mode 100644 src/android/DBManager.java create mode 100644 src/android/DBRequest.java create mode 100644 src/android/DBRunner.java create mode 100644 src/android/PawSQLite.java create mode 100644 src/android/QueryWrapper.java create mode 100644 src/log.mjs create mode 100644 src/psql_adapter.mjs create mode 100644 src/psql_adapter_error.mjs create mode 100644 webpack.config.js diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..c6619f1 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,3 @@ +{ + "esversion": 9 +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100755 index 0000000..274fff6 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "pawsqlite-cordova-adapter", + "version": "1.0.0", + "description": "Cordova adapter for PawSQLite", + "cordova": { + "id": "org.n0m.pawsqlite", + "platforms": [ + "android" + ] + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "webpack" + }, + "repository": { + "type": "git", + "url": "https://git.n0m.org/n0m/PawSQLite-Cordova-Adapter.git" + }, + "keywords": [ + "cordova", + "sqlite", + "ecosystem:cordova", + "cordova-android" + ], + "author": "Ben Ashton", + "license": "MIT", + "devDependencies": { + "webpack": "5.x", + "webpack-cli": "4.x" + } +} \ No newline at end of file diff --git a/plugin.xml b/plugin.xml new file mode 100644 index 0000000..225bf5f --- /dev/null +++ b/plugin.xml @@ -0,0 +1,30 @@ + + + PawSQLite + Cordova SQLite Plugin + + cordova,sqlite + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/android/CallbackWrapper.java b/src/android/CallbackWrapper.java new file mode 100644 index 0000000..69191c2 --- /dev/null +++ b/src/android/CallbackWrapper.java @@ -0,0 +1,72 @@ +package org.n0m.pawsqlite; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.StringWriter; +import java.io.PrintWriter; + +import org.apache.cordova.CallbackContext; + + +public class CallbackWrapper { + private CallbackContext callbackContext; + + CallbackWrapper(CallbackContext callbackContext) { + this.callbackContext = callbackContext; + } + + public void error(String name) { + error(name, null, null); + } + public void error(String name, String message) { + error(name, message, null); + } + + public void error(String name, Exception e) { + String message = null; + if (e != null) { + message = e.getMessage(); + } + error(name, message, e); + } + + public void error(String name, String message, Exception e) { + try { + JSONObject response = new JSONObject(); + response.put("name", name); + + if (message != null) { + response.put("message", message); + } + + if (e != null) { + StringWriter stringWriter = new StringWriter(); + e.printStackTrace(new PrintWriter(stringWriter)); + response.put("trace", stringWriter.toString()); + } + + callbackContext.error(response); + } catch (JSONException jsonException) { + callbackContext.error("Encountered error and unable to generate " + + "response"); + } + } + + public void success() { + JSONObject response = new JSONObject(); + success(response); + } + + public void success(JSONObject response) { + if (!response.has("success")) { + try { + response.put("success", true); + } catch (JSONException e) { + // Will never happen + } + } + callbackContext.success(response); + } +} \ No newline at end of file diff --git a/src/android/DB.java b/src/android/DB.java new file mode 100644 index 0000000..645a3c2 --- /dev/null +++ b/src/android/DB.java @@ -0,0 +1,132 @@ +package org.n0m.pawsqlite; + +import java.io.File; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.util.Log; + +import android.database.sqlite.SQLiteDatabase; +import android.database.SQLException; +import android.database.Cursor; + + +class DB { + private static final String TAG = "PawSQLite"; + + public final String dbName; + private File dbFile; + + public SQLiteDatabase db; + + + public DB (File dbFile, String dbName) { + this.dbFile = dbFile; + this.dbName = dbName; + } + + public void handleRequest(DBRequest request) { + try { + switch(request.action) { + case VERSION: + version(request.callback); + break; + case OPEN: + open(request.callback); + break; + case SQL: + sql(request.args, request.callback); + break; + case CLOSE: + close(request.callback); + break; + case DELETE: + delete(request.callback); + break; + default: + request.callback.error("Unrecognised Action"); + } + } catch (JSONException e) { + request.callback.error("JSONException", e); + } + } + + public void open(CallbackWrapper callback) throws JSONException { + if (db == null || !db.isOpen()) { + db = SQLiteDatabase.openOrCreateDatabase(dbFile, null); + } + + JSONObject response = new JSONObject(); + response.put("path", db.getPath()); + response.put("version", db.getVersion()); + callback.success(response); + } + + + public void delete(CallbackWrapper callback) { + if (db == null) { + if(SQLiteDatabase.deleteDatabase(dbFile)) { + callback.success(); + } else { + callback.error("Unable to Delete Database", + "SQLiteDatabase.deleteDatabase returned false"); + } + } else { + callback.error("Unable to Delete Database", + "Database is currently open"); + } + } + + public void sql(JSONArray args, CallbackWrapper callback) throws JSONException { + if (db == null) { + callback.error( + "DB Not open", + "Database: " + dbName + " is not open" + ); + } + + String query = args.optString(0); + // Remove query from args + args.remove(0); + + QueryWrapper queryWrapper = new QueryWrapper(db, query, args); + + JSONObject result; + try { + result = queryWrapper.execute(); + } catch (SQLException e) { + callback.error("SQLException", e); + return; + } catch (Exception e) { + callback.error("Exception", e); + return; + } + + callback.success(result); + } + + public void close(CallbackWrapper callback) { + if (db != null) { + db.close(); + } + callback.success(); + } + + public void version(CallbackWrapper callback) throws JSONException { + String query = "select sqlite_version() AS sqlite_version"; + SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(":memory:", null); + Cursor cursor = db.rawQuery(query, null); + String sqliteVersion = ""; + if (cursor.moveToNext()) { + sqliteVersion = cursor.getString(0); + } + cursor.close(); + db.close(); + + JSONObject result = new JSONObject(); + result.put("version", sqliteVersion); + callback.success(result); + } +} \ No newline at end of file diff --git a/src/android/DBAction.java b/src/android/DBAction.java new file mode 100644 index 0000000..55a2f01 --- /dev/null +++ b/src/android/DBAction.java @@ -0,0 +1,9 @@ +package org.n0m.pawsqlite; + +public enum DBAction { + VERSION, + OPEN, + CLOSE, + DELETE, + SQL +} \ No newline at end of file diff --git a/src/android/DBManager.java b/src/android/DBManager.java new file mode 100644 index 0000000..f043985 --- /dev/null +++ b/src/android/DBManager.java @@ -0,0 +1,44 @@ +package org.n0m.pawsqlite; + +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.cordova.CordovaInterface; + +import java.io.File; + +public class DBManager { + private static final String TAG = "PawSQLite"; + + private CordovaInterface cordova; + private ConcurrentHashMap dbRunnerMap = + new ConcurrentHashMap(); + + + DBManager(CordovaInterface cordova) { + this.cordova = cordova; + } + + public void queueRequest(String dbName, DBRequest request) { + DBRunner dbRunner = dbRunnerMap.get(dbName); + + if (dbRunner == null) { + File dbFile = cordova.getActivity().getDatabasePath(dbName); + dbRunner = new DBRunner(dbFile, dbName); + this.cordova.getThreadPool().execute(dbRunner); + dbRunnerMap.put(dbName, dbRunner); + } + + dbRunner.queueRequest(request); + + // Remove dbRunner if it is being closed + if (request.action == DBAction.CLOSE || + request.action == DBAction.DELETE) { + + this.remove(dbName); + } + } + + public void remove(String dbName) { + dbRunnerMap.remove(dbName); + } +} \ No newline at end of file diff --git a/src/android/DBRequest.java b/src/android/DBRequest.java new file mode 100644 index 0000000..28a1033 --- /dev/null +++ b/src/android/DBRequest.java @@ -0,0 +1,18 @@ +package org.n0m.pawsqlite; + +import org.json.JSONArray; + + +public class DBRequest { + private static final String TAG = "PawSQLite"; + + public DBAction action; + public JSONArray args; + public CallbackWrapper callback; + + DBRequest(DBAction action, JSONArray args, CallbackWrapper callback) { + this.action = action; + this.args = args; + this.callback = callback; + } +} \ No newline at end of file diff --git a/src/android/DBRunner.java b/src/android/DBRunner.java new file mode 100644 index 0000000..fcbb0ec --- /dev/null +++ b/src/android/DBRunner.java @@ -0,0 +1,50 @@ +package org.n0m.pawsqlite; + +import java.io.File; +import java.util.concurrent.LinkedBlockingQueue; + +import android.util.Log; + + +public class DBRunner implements Runnable { + private static final String TAG = "PawSQLite"; + + private LinkedBlockingQueue queue; + + public String dbName; + private File dbFile; + + DBRunner(File dbFile, final String dbName) { + queue = new LinkedBlockingQueue(); + this.dbFile = dbFile; + this.dbName = dbName; + } + + public void queueRequest(DBRequest request) { + try { + queue.put(request); + } catch (InterruptedException e) { + Log.e(TAG, "Unexpected Error", e); + request.callback.error( + "Unexpected Error", + "Thread Interrupted", + e + ); + } + } + + public void run() { + DBRequest request; + DB db = new DB(dbFile, dbName); + + while(true) { + try { + request = queue.take(); + } catch (InterruptedException e) { + Log.e(TAG, "Unexpected Error", e); + continue; + } + db.handleRequest(request); + } + } +} \ No newline at end of file diff --git a/src/android/PawSQLite.java b/src/android/PawSQLite.java new file mode 100644 index 0000000..83a02d0 --- /dev/null +++ b/src/android/PawSQLite.java @@ -0,0 +1,67 @@ +package org.n0m.pawsqlite; + +import android.util.Log; + +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.CallbackContext; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + + +public class PawSQLite extends CordovaPlugin { + private static final String TAG = "PawSQLite"; + + DBManager dbManager; + + @Override + protected void pluginInitialize() { + Log.d(TAG, "Initialized Plugin"); + this.dbManager = new DBManager(this.cordova); + } + + @Override + public boolean execute(String actionStr, JSONArray args, CallbackContext callbackContext) throws JSONException { + CallbackWrapper callback = new CallbackWrapper(callbackContext); + + // Get Action + DBAction action; + try { + action = DBAction.valueOf(actionStr.toUpperCase()); + } catch (IllegalArgumentException e) { + return false; + } + + // Get DB Name + String dbName = args.optString(0).trim(); + + if (dbName.isEmpty()) { + callback.error( + "Unknown Database", + "Database name not included in request" + ); + return true; + } + + // Remove dbName so that remaining args can be handled appropriately + args.remove(0); + + // Queue request + DBRequest request = new DBRequest(action, args, callback); + dbManager.queueRequest(dbName, request); + + return true; + } + + private JSONObject jsonError(String name, String message) throws JSONException { + JSONObject response = new JSONObject(); + + JSONObject error = new JSONObject(); + error.put("name", name); + error.put("message", message); + response.put("error", error); + + return response; + } +} diff --git a/src/android/QueryWrapper.java b/src/android/QueryWrapper.java new file mode 100644 index 0000000..69ccc69 --- /dev/null +++ b/src/android/QueryWrapper.java @@ -0,0 +1,188 @@ +package org.n0m.pawsqlite; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; +import android.database.SQLException; +import android.database.Cursor; + + +class QueryWrapper { + static enum QueryType { + SELECT, + INSERT, + UPDATE, + DELETE, + BEGIN, + COMMIT, + ROLLBACK, + OTHER + } + + SQLiteDatabase db; + String query; + QueryType queryType; + JSONArray args; + + public QueryWrapper(SQLiteDatabase db, String query, JSONArray args) { + this.db = db; + this.args = args; + + query = query.trim(); + + // Strip trailing semi-colon + if (query.endsWith(";")) { + query = query.substring(0, query.length() - 1); + } + + String operation = query + .replaceAll("[^a-zA-Z].*", "") + .toUpperCase(); + + try { + queryType = QueryType.valueOf(operation); + } catch (IllegalArgumentException e) { + queryType = QueryType.OTHER; + } + + this.query = query; + } + + public JSONObject execute() throws JSONException, SQLException { + JSONObject result = new JSONObject(); + + result.put("query", query); + result.put("args", args); + + switch (queryType) { + case SELECT: + executeRaw(result); + break; + case INSERT: + executeInsert(result); + break; + case UPDATE: + case DELETE: + executeUpdateDelete(result); + break; + default: + executeOther(result); + break; + } + + return result; + } + + private JSONObject executeInsert(JSONObject result) throws JSONException, SQLException { + SQLiteStatement statement = compileStatement(); + + long rowId = statement.executeInsert(); + + result.put("insertId", rowId); + + // Allow chaining + return result; + } + + + private JSONObject executeUpdateDelete(JSONObject result) throws JSONException, SQLException { + SQLiteStatement statement = compileStatement(); + + int rowsAffected = statement.executeUpdateDelete(); + + result.put("rowsAffected", rowsAffected); + + // Allow chaining + return result; + } + + private JSONObject executeOther(JSONObject result) throws JSONException, SQLException { + SQLiteStatement statement = compileStatement(); + statement.execute(); + return result; + } + + + private SQLiteStatement compileStatement() throws JSONException, SQLException { + SQLiteStatement statement = db.compileStatement(query); + + for (int i = 0; i < args.length(); i++) { + if (args.get(i) instanceof Float || args.get(i) instanceof Double) { + statement.bindDouble(i + 1, args.getDouble(i)); + } else if (args.get(i) instanceof Number) { + statement.bindLong(i + 1, args.getLong(i)); + } else if (args.isNull(i)) { + statement.bindNull(i + 1); + } else { + statement.bindString(i + 1, args.getString(i)); + } + } + + return statement; + } + + + private JSONObject executeRaw(JSONObject result) throws JSONException { + String[] stringArgs = new String[args.length()]; + + for (int i = 0; i < args.length(); i++) { + if (args.isNull(i)) { + stringArgs[i] = ""; + } else { + stringArgs[i] = args.getString(i); + } + } + + Cursor cur = db.rawQuery(query, stringArgs); + + if (cur != null) { + JSONArray rows = cursorToJSONArray(cur); + result.put("rows", rows); + cur.close(); + } + + // Allow chaining + return result; + } + + private JSONArray cursorToJSONArray(Cursor cur) throws JSONException { + JSONArray rows = new JSONArray(); + + if (cur != null && cur.moveToFirst()) { + int colCount = cur.getColumnCount(); + + do { + JSONObject row = new JSONObject(); + + for (int i = 0; i < colCount; i++) { + String key = cur.getColumnName(i); + + switch(cur.getType(i)) { + 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: + case Cursor.FIELD_TYPE_BLOB: + default: + row.put(key, cur.getString(i)); + break; + } + } + + rows.put(row); + } + while (cur.moveToNext()); + } + + return rows; + } +} \ No newline at end of file diff --git a/src/log.mjs b/src/log.mjs new file mode 100644 index 0000000..8eb0efc --- /dev/null +++ b/src/log.mjs @@ -0,0 +1,14 @@ +let DEBUG = false; + +export function log(...args) { + if (DEBUG) { + console.log(...args); + } +} + +export function enableDebug(active) { + DEBUG = !!active; + log("PawSQLite-Cordova-Adapter: debugging " + ( + DEBUG ? "enabled" : "disabled") + ); +} \ No newline at end of file diff --git a/src/psql_adapter.mjs b/src/psql_adapter.mjs new file mode 100644 index 0000000..7f872b3 --- /dev/null +++ b/src/psql_adapter.mjs @@ -0,0 +1,31 @@ +import { PSQLAdapterError } from "./psql_adapter_error.mjs"; +import { log, enableDebug } from "./log.mjs"; + + +export const PSQLAdapter = { + name: "PawSQLiteCordovaAdapter", + + open: (dbName) => new Promise((resolve, reject) => { + cordova.exec(resolve, (e) => { + reject(new PSQLAdapterError(e)); + }, "PawSQLite", "open", [dbName]); + }), + close: (dbName) => new Promise((resolve, reject) => { + cordova.exec(resolve, (e) => { + reject(new PSQLAdapterError(e)); + }, "PawSQLite", "close", [dbName]); + }), + sql: (dbName, sql, ...args) => new Promise((resolve, reject) => { + log(sql); + cordova.exec(resolve, (e) => { + reject(new PSQLAdapterError(e)); + }, "PawSQLite", "sql", [dbName, sql, ...args]); + }), + delete: (dbName) => new Promise((resolve, reject) => { + cordova.exec(resolve, (e) => { + reject(new PSQLAdapterError(e)); + }, "PawSQLite", "delete", [dbName]); + }), + + debug: enableDebug +}; diff --git a/src/psql_adapter_error.mjs b/src/psql_adapter_error.mjs new file mode 100644 index 0000000..b2b5040 --- /dev/null +++ b/src/psql_adapter_error.mjs @@ -0,0 +1,28 @@ +export class PSQLAdapterError extends Error { + constructor(response) { + if (response.hasOwnProperty("message")) { + super(response.message); + } else { + super(); + } + if (response.hasOwnProperty("name")) { + this.name = response.name; + } else { + this.name = "PSQLAdapterError"; + } + if (response.hasOwnProperty("trace")) { + this.trace = response.trace; + } + } + + toString() { + let str = this.name; + if (this.hasOwnProperty("message")) { + str += ": " + this.message; + } + if (this.hasOwnProperty("trace")) { + str += "\n" + this.trace; + } + return str; + } +} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..163db71 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,17 @@ +var webpack = require('webpack'); +var libraryName = 'pawsqlite-cordova-adapter'; +var outputFile = libraryName + '.js'; + +var config = { + mode: 'development', + entry: __dirname + '/src/psql_adapter.mjs', + devtool: 'source-map', + output: { + path: __dirname + '/www', + filename: outputFile, + libraryExport: 'default', + libraryTarget: 'commonjs2', + } +}; + +module.exports = config;