/* * Copyright 2013 cruxframework.org. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package org.cruxframework.crux.core.client.db; import java.util.logging.Level; import org.cruxframework.crux.core.client.collection.Array; import org.cruxframework.crux.core.client.collection.CollectionFactory; import org.cruxframework.crux.core.client.db.Cursor.CursorDirection; import org.cruxframework.crux.core.client.db.Transaction.Mode; import org.cruxframework.crux.core.client.db.websql.SQLError; import org.cruxframework.crux.core.client.db.websql.SQLResultSet; import org.cruxframework.crux.core.client.db.websql.SQLTransaction; import org.cruxframework.crux.core.client.db.websql.SQLTransaction.SQLStatementErrorCallback; import org.cruxframework.crux.core.client.utils.JsUtils; import org.cruxframework.crux.core.client.utils.StringUtils; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JsArrayMixed; import com.google.gwt.json.client.JSONObject; import com.google.gwt.logging.client.LogConfiguration; /** * @author Thiago da Rosa de Bustamante * */ public abstract class WSQLAbstractObjectStore<K, V> extends AbstractObjectStore<K, V> { protected final String name; protected final WSQLTransaction transaction; protected final WSQLAbstractDatabase db; protected Array<String> indexAndKeyColumnNames; protected Array<String> keyPath; protected Array<String> indexColumnNames; protected WSQLAbstractObjectStore(WSQLAbstractDatabase db, String name, WSQLTransaction transaction) { super(db); this.db = db; this.name = name; this.transaction = transaction; this.keyPath = getKeyPath(); this.indexColumnNames = getIndexedColumnNames(); this.indexAndKeyColumnNames = getIndexAndKeyColumnNames(); } @Override public String getObjectStoreName() { return name; } @Override public void add(final V object, final DatabaseWriteCallback<K> callback) { transaction.addRequest(new WSQLTransaction.RequestOperation() { @Override public void doOperation(final SQLTransaction tx) { insertObject(object, tx, callback, getKey(object)); } }, new Mode[]{Mode.readWrite}); } @Override public void put(final V object, final DatabaseWriteCallback<K> callback) { transaction.addRequest(new WSQLTransaction.RequestOperation() { @Override public void doOperation(final SQLTransaction tx) { updateObject(object, tx, callback, getKey(object)); } }, new Mode[]{Mode.readWrite}); } @Override public void get(final K key, final DatabaseRetrieveCallback<V> callback) { transaction.addRequest(new WSQLTransaction.RequestOperation() { @Override public void doOperation(final SQLTransaction tx) { StringBuilder sql = new StringBuilder("SELECT * FROM \"").append(name).append("\""); final JsArrayMixed args = JsArrayMixed.createArray().cast(); if (key != null) { sql.append(" WHERE "); addKeyToQuery(key, sql, args); } runSelectSQL(callback, tx, args, sql.toString(), key); } }, new Mode[]{Mode.readOnly, Mode.readWrite}); } @Override public void delete(final KeyRange<K> range, final DatabaseDeleteCallback callback) { if (callback != null) { callback.setDb(db); } transaction.addRequest(new WSQLTransaction.RequestOperation() { @Override public void doOperation(final SQLTransaction tx) { StringBuilder sql = new StringBuilder("DELETE FROM \"").append(name).append("\""); JsArrayMixed args = JsArrayMixed.createArray().cast(); if (range != null) { addKeyRangeToQuery(range, sql, args); } String sqlStatement = sql.toString(); runDeleteSQL(callback, tx, args, sqlStatement); } }, new Mode[]{Mode.readWrite}); } @Override public void delete(final K key, final DatabaseDeleteCallback callback) { if (callback != null) { callback.setDb(db); } transaction.addRequest(new WSQLTransaction.RequestOperation() { @Override public void doOperation(final SQLTransaction tx) { StringBuilder sql = new StringBuilder("DELETE FROM \"").append(name).append("\""); JsArrayMixed args = JsArrayMixed.createArray().cast(); if (key != null) { sql.append(" WHERE "); addKeyToQuery(key, sql, args); } String sqlStatement = sql.toString(); runDeleteSQL(callback, tx, args, sqlStatement); } }, new Mode[]{Mode.readWrite}); } @Override public void clear(final DatabaseCallback callback) { if (callback != null) { callback.setDb(db); } transaction.addRequest(new WSQLTransaction.RequestOperation() { @Override public void doOperation(final SQLTransaction tx) { String sql = "DELETE FROM __sys__ WHERE name = ?"; JsArrayMixed args = JsArrayMixed.createArray().cast(); args.push(name); if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Running SQL ["+sql+"]"); } tx.executeSQL(sql, args, null, getErrorHandler(callback)); } }, new Mode[]{Mode.readWrite}); transaction.addRequest(new WSQLTransaction.RequestOperation() { @Override public void doOperation(final SQLTransaction tx) { String sql = "DELETE FROM \"" + name + "\""; JsArrayMixed args = JsArrayMixed.createArray().cast(); runDatabaseSQL(callback, tx, args, sql); } }, new Mode[]{Mode.readWrite}); } public void createTable(SQLTransaction tx, final DatabaseCallback callback) { if (callback != null) { callback.setDb(db); } String sql = "INSERT INTO __sys__(name) VALUES (?)"; JsArrayMixed args = JsArrayMixed.createArray().cast(); args.push(name); if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Running SQL ["+sql+"]"); } tx.executeSQL(sql, args, new SQLTransaction.SQLStatementCallback() { @Override public void onSuccess(SQLTransaction tx, SQLResultSet rs) { String sql = getCreateTableSQL(); JsArrayMixed args = JsArrayMixed.createArray().cast(); runDatabaseSQL(callback, tx, args, sql); } }, getErrorHandler(callback)); } @Override public void count(final DatabaseCountCallback callback) { count(null, callback); } @Override public void count(final KeyRange<K> range, final DatabaseCountCallback callback) { if (callback != null) { callback.setDb(db); } transaction.addRequest(new WSQLTransaction.RequestOperation() { @Override public void doOperation(final SQLTransaction tx) { StringBuilder sql = new StringBuilder("SELECT COUNT(*) AS total FROM \"").append(name).append("\""); JsArrayMixed args = JsArrayMixed.createArray().cast(); if (range != null) { addKeyRangeToQuery(range, sql, args); } String sqlStatement = sql.toString(); if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Running SQL ["+sqlStatement+"]"); } tx.executeSQL(sqlStatement, args, new SQLTransaction.SQLStatementCallback() { @Override public void onSuccess(SQLTransaction tx, SQLResultSet rs) { if (callback != null) { try { int count; if (rs.getRows().length() > 0) { count = JsUtils.readIntPropertyValue(rs.getRows().itemObject(0), "total"); } else { String message = db.messages.objectStoreCountError("No rows returned"); if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.SEVERE, message); } callback.onError(message); callback.setDb(null); return; } if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "There are ["+count+"] records on object store ["+name+"]."); } callback.onSuccess(count); callback.setDb(null); } catch (Exception e) { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.SEVERE, db.messages.objectStoreOperationError(e.getMessage()), e); } } } } }, getErrorHandler(callback)); } }, new Mode[]{Mode.readOnly, Mode.readWrite}); } @Override public void openCursor(DatabaseCursorCallback<K, V> callback) { openCursor(null, CursorDirection.next, callback); } @Override public void openCursor(KeyRange<K> keyRange, DatabaseCursorCallback<K, V> callback) { openCursor(keyRange, CursorDirection.next, callback); } protected void insertObject(final V object, final SQLTransaction tx, final DatabaseWriteCallback<K> callback, final K key) { encodeObject(object, new EncodeCallback() { @Override public void onEncode(JSONObject encoded) { insertObject(tx, callback, key, encoded); } }); } protected void insertObject(final SQLTransaction tx, final DatabaseWriteCallback<K> callback, final K key, JSONObject encoded) { StringBuilder sqlStart = new StringBuilder("INSERT INTO ").append("\""+ name +"\" ("); StringBuilder sqlEnd = new StringBuilder(" VALUES("); JavaScriptObject encodedObject = encoded.getJavaScriptObject(); JsArrayMixed sqlValues = JsArrayMixed.createArray().cast(); if (!isAutoIncrement() || key != null) { getIndexesValuesForObject(encodedObject, indexAndKeyColumnNames, sqlValues); for (int i=0; i< indexAndKeyColumnNames.size(); i++) { String k = indexAndKeyColumnNames.get(i); sqlStart.append("\""+k +"\","); sqlEnd.append("?,"); } } else { getIndexesValuesForObject(encodedObject, indexColumnNames, sqlValues); for (int i=0; i< indexColumnNames.size(); i++) { String k = indexColumnNames.get(i); sqlStart.append("\""+k +"\","); sqlEnd.append("?,"); } } sqlStart.append("value)"); sqlEnd.append("?)"); sqlValues.push(encoded.toString()); String sqlStatement = sqlStart.toString()+" "+sqlEnd.toString() ; runInsertQL(callback, tx, sqlValues, sqlStatement, key); } protected void updateObject(final V object, final SQLTransaction tx, final DatabaseWriteCallback<K> callback, final K key) { encodeObject(object, new EncodeCallback() { @Override public void onEncode(JSONObject encoded) { StringBuilder sql = new StringBuilder("UPDATE ").append("\""+ name +"\" SET "); JsArrayMixed sqlValues = JsArrayMixed.createArray().cast(); getIndexesValuesForObject(encoded.getJavaScriptObject(), indexColumnNames, sqlValues); for (int i=0; i< indexColumnNames.size(); i++) { String key = indexColumnNames.get(i); sql.append("\""+key +"\" = ?, "); } sql.append("value = ?"); sqlValues.push(encoded.toString()); sql.append(" WHERE "); addKeyToQuery(key, sql, sqlValues); String sqlStatement = sql.toString(); runUpdateSQL(callback, tx, sqlValues, sqlStatement, key, encoded); } }); } protected Array<String> getIndexAndKeyColumnNames() { Array<String> indexAndKeys = CollectionFactory.createArray(); for (int i=0; i< keyPath.size(); i++) { String k = keyPath.get(i); indexAndKeys.add(k); } for (int i=0; i< indexColumnNames.size(); i++) { String k = indexColumnNames.get(i); if (indexAndKeys.indexOf(k) == -1) { indexAndKeys.add(k); } } return indexAndKeys; } protected void getIndexesValuesForObject(JavaScriptObject object, Array<String> columnNames, JsArrayMixed output) { for (int i=0; i<columnNames.size(); i++) { JsUtils.readPropertyValue(object, columnNames.get(i), output, true); } } protected void runUpdateSQL(final DatabaseWriteCallback<K> callback, final SQLTransaction tx, JsArrayMixed args, String sqlStatement, final K key, final JSONObject encodedObject) { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Running SQL ["+sqlStatement+"]"); } tx.executeSQL(sqlStatement, args, new SQLTransaction.SQLStatementCallback() { @Override public void onSuccess(SQLTransaction tx, SQLResultSet rs) { if (rs.getRowsAffected() == 0) { insertObject(tx, callback, key, encodedObject); } else { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Operation executed on database. Table ["+name+"]"); } if (callback != null) { try { callback.onSuccess(key); callback.setDb(null); } catch (Exception e) { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.SEVERE, db.messages.objectStoreOperationError(e.getMessage()), e); } } } } } }, getErrorHandler(callback)); } protected void runInsertQL(final DatabaseWriteCallback<K> callback, final SQLTransaction tx, JsArrayMixed args, String sqlStatement, final K key) { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Running SQL ["+sqlStatement+"]"); } tx.executeSQL(sqlStatement, args, new SQLTransaction.SQLStatementCallback() { @SuppressWarnings("unchecked") @Override public void onSuccess(SQLTransaction tx, SQLResultSet rs) { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Operation executed on database. Table ["+name+"]"); } if (callback != null) { try { if (isAutoIncrement()) { // Only integer keys can be auto incremented. callback.onSuccess((K) new Integer(rs.getInsertId())); } else { callback.onSuccess(key); } callback.setDb(null); } catch (Exception e) { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.SEVERE, db.messages.objectStoreOperationError(e.getMessage()), e); } } } } }, getErrorHandler(callback)); } protected void runDeleteSQL(final DatabaseDeleteCallback callback, final SQLTransaction tx, JsArrayMixed args, String sqlStatement) { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Running SQL ["+sqlStatement+"]"); } tx.executeSQL(sqlStatement, args, new SQLTransaction.SQLStatementCallback() { @Override public void onSuccess(SQLTransaction tx, SQLResultSet rs) { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Operation executed on database. Table ["+name+"]"); } if (callback != null) { try { callback.onSuccess(); callback.setDb(null); } catch (Exception e) { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.SEVERE, db.messages.objectStoreOperationError(e.getMessage()), e); } } } } }, getErrorHandler(callback)); } protected void runSelectSQL(final DatabaseRetrieveCallback<V> callback, final SQLTransaction tx, JsArrayMixed args, String sqlStatement, final K key) { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Running SQL ["+sqlStatement+"]"); } tx.executeSQL(sqlStatement, args, new SQLTransaction.SQLStatementCallback() { @Override public void onSuccess(SQLTransaction tx, SQLResultSet rs) { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Operation executed on database. Table ["+name+"]"); } if (callback != null) { try { if (rs.getRows().length() > 0) { String encodedObject = JsUtils.readStringPropertyValue(rs.getRows().itemObject(0), "value"); if (!StringUtils.isEmpty(encodedObject)) { V object = decodeObject(encodedObject); if (isAutoIncrement() && key != null) { setObjectKey(object, key); } callback.onSuccess(object); } else { callback.onError(db.messages.objectStoreOperationError("Read error")); return; } } else { callback.onSuccess(null); } callback.setDb(null); } catch (Exception e) { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.SEVERE, db.messages.objectStoreOperationError(e.getMessage()), e); } } } } }, getErrorHandler(callback)); } protected void runDatabaseSQL(final DatabaseCallback callback, final SQLTransaction tx, JsArrayMixed args, String sqlStatement) { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Running SQL ["+sqlStatement+"]"); } tx.executeSQL(sqlStatement, args, new SQLTransaction.SQLStatementCallback() { @Override public void onSuccess(SQLTransaction tx, SQLResultSet rs) { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Operation executed on database. Table ["+name+"]"); } if (callback != null) { try { callback.onSuccess(); callback.setDb(null); } catch (Exception e) { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.SEVERE, db.messages.objectStoreOperationError(e.getMessage()), e); } } } } }, getErrorHandler(callback)); } protected SQLStatementErrorCallback getErrorHandler(final Callback callback) { return new SQLTransaction.SQLStatementErrorCallback() { @Override public boolean onError(SQLTransaction tx, SQLError error) { String message = db.messages.objectStoreOperationError(error.getName() + " - " + error.getMessage()); if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.SEVERE, message); } if (callback != null) { callback.onError(message); callback.setDb(null); } else if (db.errorHandler != null) { db.errorHandler.onError(message); } return true; } }; } protected void reportError(String message) { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.SEVERE, message); } if (db.errorHandler != null) { db.errorHandler.onError(message); } } protected abstract Array<String> getIndexedColumnNames(); protected abstract Array<String> getKeyPath(); protected abstract void addKeyRangeToQuery(final KeyRange<K> range, StringBuilder sql, JsArrayMixed args); protected abstract void addKeyToQuery(final K key, StringBuilder sql, JsArrayMixed args); protected abstract K getKey(V object); protected abstract void setObjectKey(V object, K key); protected abstract V decodeObject(String encodedObject); protected abstract void encodeObject(V object, EncodeCallback callback); protected abstract String getCreateTableSQL(); public static interface EncodeCallback { void onEncode(JSONObject encoded); } }