/**
* This file is part of Horaz.
*
* Horaz is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Horaz is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Horaz. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright Joe's App Factory UG (haftungsbeschränkt)
*/
package com.horaz.client.model;
import java.util.Date;
import com.google.code.gwt.database.client.Database;
import com.google.code.gwt.database.client.SQLError;
import com.google.code.gwt.database.client.SQLResultSet;
import com.google.code.gwt.database.client.SQLTransaction;
import com.google.code.gwt.database.client.StatementCallback;
import com.google.code.gwt.database.client.TransactionCallback;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.event.shared.GwtEvent.Type;
import com.google.gwt.event.shared.HandlerRegistration;
import com.horaz.client.model.events.ReadyEvent;
import com.horaz.client.model.events.ReadyListener;
import com.horaz.client.model.events.TableCreatedEvent;
import com.horaz.client.model.events.TableCreatedListener;
public abstract class SQLiteDataStore<T extends BaseModel> extends DataStore<T> implements AsynchronousDataStore<T> {
static private class GetCountJS extends JavaScriptObject {
protected GetCountJS() {
}
public final native int getCount() /*-{ return this._count!==undefined ? this._count : 1; }-*/;
}
static public class ModelWrapperJS extends JavaScriptObject {
protected ModelWrapperJS() {
}
public final native boolean getFieldBoolean(String field) /*-{ return this[field]==1; }-*/;
public final native float getFieldFloat(String field) /*-{ return parseFloat(this[field]); }-*/;
public final native int getFieldInteger(String field) /*-{ return this[field]; }-*/;
public final native String getFieldString(String field) /*-{ return this[field]; }-*/;
public final native int getModelId() /*-{ return this.modelId; }-*/;
}
public static class SQLiteColumnDef {
public enum Type {
TEXT, NUMERIC, INTEGER, REAL, _MODEL_ID
}
private final String columnName;
private final Type typeName;
public SQLiteColumnDef(String columnName, Type typeName) {
this.columnName = columnName;
this.typeName = typeName;
}
public String getColumnName() {
return columnName;
}
public Type getTypeName() {
return typeName;
}
public String toSQL() {
return columnName+" "+(Type._MODEL_ID == typeName ? "INTEGER PRIMARY KEY" : typeName.name());
}
}
private final Database database;
private String table;
private SQLiteColumnDef[] tableColumns; // includes modelId
private long lastModelId;
private boolean ready;
private String joinStatement;
private SQLiteDataStore<BaseModel> joinedDataStore;
public SQLiteDataStore(String databaseName, String version, int maxSizeBytes) {
if (!Database.isSupported()) {
throw new IllegalStateException("HTML5 Database is not supported by this browser.");
}
database = Database.openDatabase(databaseName, version, databaseName, maxSizeBytes);
}
@Override
public void add(final T newModel) {
if (!ready) throw new IllegalStateException("Table was not initialized, yet.");
// set model id
newModel.setField("modelId", ++lastModelId);
database.transaction(new TransactionCallback() {
@Override
public void onTransactionFailure(SQLError error) {
throw new IllegalStateException(error.getMessage());
}
@Override
public void onTransactionStart(SQLTransaction transaction) {
String cols = "";
String values = "";
Object[] args = new Object[tableColumns.length];
int i=0;
for (SQLiteColumnDef colDef : tableColumns) {
cols += "," + colDef.columnName;
values += ",?";
// TODO types
Object value = newModel.getRawField(colDef.columnName);
if (value instanceof Boolean) {
args[i] = ((Boolean) value)?1:0;
} else if (value instanceof Date) {
args[i] = ((Date) value).getTime()/1000;
} else {
args[i] = value;
}
i++;
}
String sqlStmt = "INSERT INTO "+table+" ("+cols.substring(1)+") VALUES ("+values.substring(1)+")";
System.out.println(sqlStmt);
transaction.executeSql(sqlStmt, args);
}
@Override
public void onTransactionSuccess() {
// join
if (joinStatement != null) {
find("modelId", newModel.getModelId(), new FindCallback<T>() {
@Override
public void onSuccess(ModelsCollection<T> results) {
added(results.iterator().next());
}
});
} else {
added(newModel);
}
}
});
}
public HandlerRegistration addReadyListener(ReadyListener handler) {
Type<ReadyListener> type = ReadyEvent.getType();
return handlerManager.addHandler(type, handler);
}
public HandlerRegistration addTableCreatedListener(TableCreatedListener handler) {
Type<TableCreatedListener> type = TableCreatedEvent.getType();
return handlerManager.addHandler(type, handler);
}
public void dropTable(final Runnable callback) {
if (!ready) throw new IllegalStateException("Table was not initialized, yet.");
database.transaction(new TransactionCallback() {
@Override
public void onTransactionFailure(SQLError error) {
}
@Override
public void onTransactionStart(SQLTransaction transaction) {
transaction.executeSql("DROP TABLE IF EXISTS "+table, null, new StatementCallback<JavaScriptObject>() {
@Override
public boolean onFailure(SQLTransaction transaction,
SQLError error) {
throw new IllegalStateException(error.getMessage());
}
@Override
public void onSuccess(SQLTransaction transaction,
SQLResultSet<JavaScriptObject> resultSet) {
callback.run();
}
});
}
@Override
public void onTransactionSuccess() {
}
});
}
@Override
public void find(Filter filter, FindCallback<T> callback) {
findAll(filter, callback, "LIMIT 1", false);
}
@Override
public void find(String field, Object value, FindCallback<T> callback) {
findAll(new Filter().whereEquals(field, value), callback, "LIMIT 1", false);
}
@Override
public void findAll(Filter filter, FindCallback<T> callback) {
findAll(filter, callback, null, true);
}
private void findAll(Filter filter, final FindCallback<T> callback, final String customSql, final boolean useGroupBy) {
if (!ready) throw new IllegalStateException("Table was not initialized, yet.");
if (filter == null) {
filter = new Filter();
}
if (this.filter != null) {
filter.mergeFilter(this.filter);
}
final Filter fFilter = filter;
database.readTransaction(new TransactionCallback() {
@Override
public void onTransactionFailure(SQLError error) {
throw new IllegalStateException(error.getMessage());
}
@Override
public void onTransactionStart(SQLTransaction transaction) {
String sql;
// select
if (useGroupBy && groupBy != null) {
// count children
sql = "SELECT *, COUNT(*) AS _count";
} else {
// normal
sql = "SELECT *";
}
sql += " FROM "+table;
// join
if (joinStatement != null) {
// normal
sql += " JOIN "+joinStatement;
}
// where
sql += " WHERE "+fFilter.getSQLStatement(table+".");
if (useGroupBy && groupBy != null) {
// count children
sql += " GROUP BY "+groupBy;
}
if (customSql != null) {
// normal
sql += " "+customSql;
}
System.out.println(sql);
transaction.executeSql(sql, fFilter.getValues(), new StatementCallback<JavaScriptObject>() {
@Override
public boolean onFailure(SQLTransaction transaction, SQLError error) {
throw new IllegalStateException(error.getMessage());
}
@Override
public void onSuccess(SQLTransaction transaction, SQLResultSet<JavaScriptObject> resultSet) {
callback.onSuccess(new ModelsCollection<T>(SQLiteDataStore.this, joinedDataStore, resultSet));
}
});
}
@Override
public void onTransactionSuccess() {
}
});
}
public void findAll(Filter filter, String orderBy, String limit, FindCallback<T> callback) {
String sql = "";
if (orderBy != null) {
sql += "ORDER BY "+orderBy+" ";
}
if (limit != null) {
sql += "LIMIT "+limit+" ";
}
findAll(filter, callback, sql, true);
}
@Override
public void findAll(String field, Object value, FindCallback<T> callback) {
findAll(new Filter().whereEquals(field, value), callback);
}
@Override
public void get(final long id, final FindCallback<T> callback) {
if (!ready) throw new IllegalStateException("Table was not initialized, yet.");
database.readTransaction(new TransactionCallback() {
@Override
public void onTransactionFailure(SQLError error) {
throw new IllegalStateException(error.getMessage());
}
@Override
public void onTransactionStart(SQLTransaction transaction) {
transaction.executeSql("SELECT * FROM "+table+" WHERE modelId=? LIMIT 1", new Object[] {id}, new StatementCallback<JavaScriptObject>() {
@Override
public boolean onFailure(SQLTransaction transaction, SQLError error) {
throw new IllegalStateException(error.getMessage());
}
@Override
public void onSuccess(SQLTransaction transaction, SQLResultSet<JavaScriptObject> resultSet) {
callback.onSuccess(new ModelsCollection<T>(SQLiteDataStore.this, joinedDataStore, resultSet));
}
});
}
@Override
public void onTransactionSuccess() {
}
});
}
@Override
public void getChildren(T mdl, AsynchronousDataStore.FindCallback<T> callback) {
if (mdl.hasChildren()) {
Filter filter = new Filter();
filter.whereEquals(groupBy, mdl.getRawField(groupBy));
filter.whereNotEquals("modelId", mdl.getModelId()); // not itself
findAll(filter, callback, null, false);
} else {
throw new IllegalStateException("Model has no children. You have to check for children (BaseModel#hasChildren()) before calling this method.");
}
}
public Database getDatabase() {
return database;
}
protected void getLastModelId() {
// get last modelId
database.readTransaction(new TransactionCallback() {
@Override
public void onTransactionFailure(SQLError error) {
throw new IllegalStateException(error.getMessage());
}
@Override
public void onTransactionStart(SQLTransaction transaction) {
transaction.executeSql("SELECT * FROM "+table+" ORDER BY modelId DESC LIMIT 1", new Object[] {}, new StatementCallback<JavaScriptObject>() {
@Override
public boolean onFailure(SQLTransaction transaction, SQLError error) {
throw new IllegalStateException(error.getMessage());
}
@Override
public void onSuccess(SQLTransaction transaction, SQLResultSet<JavaScriptObject> resultSet) {
if (resultSet.getRows().getLength() > 0) {
lastModelId = reflectJavaScriptObject(resultSet.getRows().getItem(0)).getModelId();
}
ready = true;
fireEvent(new ReadyEvent());
}
});
}
@Override
public void onTransactionSuccess() {
}
});
}
public void initTable(String table, SQLiteColumnDef[] columns) {
this.table = table;
// add modelId col
this.tableColumns = new SQLiteColumnDef[columns.length +1];
this.tableColumns[0] = new SQLiteColumnDef("modelId", SQLiteColumnDef.Type._MODEL_ID);
for (int i=0; i<columns.length; i++) {
this.tableColumns[i+1] = columns[i];
}
database.transaction(new TransactionCallback() {
@Override
public void onTransactionFailure(SQLError error) {
if (error.getMessage().contains("already exists")) {
// table already exists
getLastModelId();
} else {
throw new IllegalStateException(error.getMessage());
}
}
@Override
public void onTransactionStart(SQLTransaction tx) {
String columnsStr = "";
for (SQLiteColumnDef col : tableColumns) {
columnsStr += "," + col.toSQL();
}
tx.executeSql("CREATE TABLE "+SQLiteDataStore.this.table+" ("
+ columnsStr.substring(1) +")", null);
}
@Override
public void onTransactionSuccess() {
// fire table created event
lastModelId = 0;
ready = true;
fireEvent(new TableCreatedEvent());
}
});
}
public boolean isReady() {
return ready;
}
@Override
public void remove(final T model) {
if (!ready) throw new IllegalStateException("Table was not initialized, yet.");
database.transaction(new TransactionCallback() {
@Override
public void onTransactionFailure(SQLError error) {
}
@Override
public void onTransactionStart(SQLTransaction transaction) {
String sqlStatement = "DELETE FROM "+table+" WHERE modelId=?";
transaction.executeSql(sqlStatement, new Object[] {model.getModelId()}, new StatementCallback<JavaScriptObject>() {
@Override
public boolean onFailure(SQLTransaction transaction,
SQLError error) {
throw new IllegalStateException(error.getMessage());
}
@Override
public void onSuccess(SQLTransaction transaction, SQLResultSet<JavaScriptObject> resultSet) {
if (resultSet.getRowsAffected() == 1) {
removed(model);
}
}
});
}
@Override
public void onTransactionSuccess() {
}
});
}
public void setJoin(SQLiteDataStore<? extends BaseModel> joinedDataStore, String joinedField) {
setJoinStatement(joinedDataStore.table+" ON "+table+"."+joinedField+"="+joinedDataStore.table+"."+joinedField);
this.joinedDataStore = (SQLiteDataStore<BaseModel>) joinedDataStore;
}
public void setJoinStatement(String joinStatement) {
this.joinStatement = joinStatement;
}
@Override
public void storeChildrenCount(T mdl, JavaScriptObject jsObj) {
mdl.setHasChildren(((GetCountJS) jsObj).getCount() > 1);
}
@Override
public void update(final T saveModel) {
if (!ready) throw new IllegalStateException("Table was not initialized, yet.");
database.transaction(new TransactionCallback() {
@Override
public void onTransactionFailure(SQLError error) {
throw new IllegalStateException(error.getMessage());
}
@Override
public void onTransactionStart(SQLTransaction transaction) {
String set = "";
Object[] args = new Object[tableColumns.length]; // exclude modelId
int i=0;
for (SQLiteColumnDef colDef : tableColumns) {
if (colDef.typeName == SQLiteColumnDef.Type._MODEL_ID) continue;
set += "," + colDef.columnName+"=?";
// TODO types
Object value = saveModel.getRawField(colDef.columnName);
if (value instanceof Boolean) {
args[i] = ((Boolean) value)?1:0;
} else if (value instanceof Date) {
args[i] = ((Date) value).getTime()/1000;
} else {
args[i] = value;
}
i++;
}
args[i] = saveModel.getModelId();
String sqlStatement = "UPDATE "+table+" SET "+set.substring(1)+" WHERE modelId=?";
transaction.executeSql(sqlStatement, args, new StatementCallback<JavaScriptObject>() {
@Override
public boolean onFailure(SQLTransaction transaction, SQLError error) {
throw new IllegalStateException(error.getMessage());
}
@Override
public void onSuccess(SQLTransaction transaction, SQLResultSet<JavaScriptObject> resultSet) {
if (resultSet.getRowsAffected() == 1) {
updated(saveModel);
}
}
});
}
@Override
public void onTransactionSuccess() {
}
});
}
}