Initial commit

This commit is contained in:
Ben Ashton 2021-03-02 20:34:02 -08:00
commit 5bf0b6752b
15 changed files with 734 additions and 0 deletions

3
.jshintrc Normal file
View File

@ -0,0 +1,3 @@
{
"esversion": 9
}

31
package.json Executable file
View File

@ -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"
}
}

30
plugin.xml Normal file
View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
id="org.n0m.pawsqlite" version="0.2.3">
<name>PawSQLite</name>
<description>Cordova SQLite Plugin</description>
<license></license>
<keywords>cordova,sqlite</keywords>
<js-module src="www/pawsqlite-cordova-adapter.js" name="PawSQLite">
<clobbers target="PawSQLite" />
</js-module>
<platform name="android">
<config-file target="res/xml/config.xml" parent="/*">
<feature name="PawSQLite">
<param name="android-package" value="org.n0m.pawsqlite.PawSQLite" />
</feature>
</config-file>
<config-file target="AndroidManifest.xml" parent="/*">
</config-file>
<source-file src="src/android/CallbackWrapper.java" target-dir="src/ca/patterpaws/sqlite" />
<source-file src="src/android/DB.java" target-dir="src/ca/patterpaws/sqlite" />
<source-file src="src/android/DBAction.java" target-dir="src/ca/patterpaws/sqlite" />
<source-file src="src/android/DBManager.java" target-dir="src/ca/patterpaws/sqlite" />
<source-file src="src/android/DBRequest.java" target-dir="src/ca/patterpaws/sqlite" />
<source-file src="src/android/DBRunner.java" target-dir="src/ca/patterpaws/sqlite" />
<source-file src="src/android/PawSQLite.java" target-dir="src/ca/patterpaws/sqlite" />
<source-file src="src/android/QueryWrapper.java" target-dir="src/ca/patterpaws/sqlite" />
</platform>
</plugin>

View File

@ -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);
}
}

132
src/android/DB.java Normal file
View File

@ -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);
}
}

View File

@ -0,0 +1,9 @@
package org.n0m.pawsqlite;
public enum DBAction {
VERSION,
OPEN,
CLOSE,
DELETE,
SQL
}

View File

@ -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<String, DBRunner> dbRunnerMap =
new ConcurrentHashMap<String, DBRunner>();
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);
}
}

View File

@ -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;
}
}

50
src/android/DBRunner.java Normal file
View File

@ -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<DBRequest> queue;
public String dbName;
private File dbFile;
DBRunner(File dbFile, final String dbName) {
queue = new LinkedBlockingQueue<DBRequest>();
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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

14
src/log.mjs Normal file
View File

@ -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")
);
}

31
src/psql_adapter.mjs Normal file
View File

@ -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
};

View File

@ -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;
}
}

17
webpack.config.js Normal file
View File

@ -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;