/* * 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 java.util.logging.Logger; import org.cruxframework.crux.core.client.collection.Array; import org.cruxframework.crux.core.client.collection.CollectionFactory; import org.cruxframework.crux.core.client.db.Transaction.Mode; import org.cruxframework.crux.core.client.db.websql.SQLDatabase; import org.cruxframework.crux.core.client.db.websql.SQLDatabaseFactory; 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.JsArrayMixed; import com.google.gwt.logging.client.LogConfiguration; /** * HTML5 AbstractDatabase based on Web SQL (http://www.w3.org/TR/webdatabase/). * @author Thiago da Rosa de Bustamante * */ public abstract class WSQLAbstractDatabase extends AbstractDatabase { private static int DEFAULT_DB_SIZE = 5 * 1024 * 1024; private static boolean initialized = false; private static SQLDatabase systemDatabase; protected static Logger logger = Logger.getLogger(WSQLAbstractDatabase.class.getName()); protected boolean opened = false; protected SQLDatabase database; protected Array<String> objectStoreNames; private static final void init(final SQLCallback callback) { if (!initialized) { systemDatabase = SQLDatabaseFactory.openDatabase("__sysdb__", "System Database", DEFAULT_DB_SIZE); systemDatabase.transaction(new SQLDatabase.SQLTransactionCallback() { @Override public void onTransaction(SQLTransaction tx) { String sql = "CREATE TABLE IF NOT EXISTS dbVersions (name VARCHAR(255), version INT)"; if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Running SQL ["+sql+"]"); } JsArrayMixed args = JsArrayMixed.createArray().cast(); tx.executeSQL(sql, args, new SQLTransaction.SQLStatementCallback() { @Override public void onSuccess(SQLTransaction tx, SQLResultSet rs) { initialized = true; if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.INFO, "System table created."); } callback.onSuccess(); } }, null); } }, new SQLDatabase.SQLTransactionErrorCallback() { @Override public void onError(SQLError error) { throw new DatabaseException("Could not create the systam table (__sysdb__). Error ["+error.getName()+"]. Message ["+error.getMessage()+"]"); } }, null); } } @Override public void close() { if (isOpen()) { opened = false; } } @Override public void delete(final DatabaseCallback callback) { if (StringUtils.isEmpty(getName())) { throw new DatabaseException(messages.databaseInvalidNameDBError(getName())); } if (!initialized) { init(new SQLCallback() { @Override public void onSuccess() { doDelete(callback); } }); } else { doDelete(callback); } } @Override public boolean isOpen() { return opened; } @Override public boolean isSupported() { return SQLDatabaseFactory.isSupported(); } @Override protected Transaction createTransaction(String[] storeNames, Mode mode) { return new WSQLTransaction(this, storeNames, mode); } @Override protected void doOpen(final DatabaseCallback callback) { if (!initialized) { init(new SQLCallback() { @Override public void onSuccess() { openDB(callback); } }); } else { openDB(callback); } } private void openDB(final DatabaseCallback callback) { final SQLTransaction.SQLStatementErrorCallback errorCallback = new SQLTransaction.SQLStatementErrorCallback() { @Override public boolean onError(SQLTransaction tx, SQLError error) { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.SEVERE, messages.databaseOpenError(getName(), error.getName()+"-"+error.getMessage())); } if (callback != null) { callback.onError(messages.databaseOpenError(getName(), error.getName()+"-"+error.getMessage())); } else if (errorHandler != null) { errorHandler.onError(messages.databaseOpenError(getName(), error.getName()+"-"+error.getMessage())); } return true; } }; systemDatabase.transaction(new SQLDatabase.SQLTransactionCallback() { @Override public void onTransaction(SQLTransaction tx) { String sql = "SELECT * FROM dbVersions WHERE name = ?"; if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Running SQL ["+sql+"]"); } final JsArrayMixed args = JsArrayMixed.createArray().cast(); args.push(name); tx.executeSQL(sql, args, new SQLTransaction.SQLStatementCallback() { @Override public void onSuccess(SQLTransaction tx, SQLResultSet rs) { if (rs.getRows().length() > 0) { JsArrayMixed output = JsArrayMixed.createArray().cast(); JsUtils.readPropertyValue(rs.getRows().itemObject(0), "version", output); doOpenDB((int) output.getNumber(0), callback, errorCallback); } else { String sql = "INSERT INTO dbVersions VALUES (?,?)"; if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Running SQL ["+sql+"]"); } args.push(version); tx.executeSQL(sql, args, new SQLTransaction.SQLStatementCallback() { @Override public void onSuccess(SQLTransaction tx, SQLResultSet rs) { doOpenDB(0, callback, errorCallback); } }, errorCallback); } } }, errorCallback); } }, null, null); } private void doOpenDB(final int oldVersion, final DatabaseCallback callback, final SQLTransaction.SQLStatementErrorCallback errorCallback) { if (version <=0 || oldVersion > version) { callback.onError("Data Error - An attempt was made to open a database using a lower version than the existing version."); return; } database = SQLDatabaseFactory.openDatabase(name, name, DEFAULT_DB_SIZE); database.transaction(new SQLDatabase.SQLTransactionCallback() { @Override public void onTransaction(SQLTransaction tx) { String sql = "CREATE TABLE IF NOT EXISTS __sys__ (name VARCHAR(255))"; if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Running SQL ["+sql+"]"); } JsArrayMixed args = JsArrayMixed.createArray().cast(); tx.executeSQL(sql, args, new SQLTransaction.SQLStatementCallback() { @Override public void onSuccess(SQLTransaction tx, SQLResultSet rs) { loadObjectStoreNames(new SQLCallback() { @Override public void onSuccess() { if (oldVersion < version) { updateDBVersionAndOpen(oldVersion, callback, errorCallback); } else { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.INFO, messages.databaseOpened(getName())); } opened = true; callback.onSuccess(); } } }); } }, errorCallback); } }, null, null); } private void loadObjectStoreNames(final SQLCallback callback) { database.transaction(new SQLDatabase.SQLTransactionCallback() { @Override public void onTransaction(final SQLTransaction tx) { String sql = "SELECT * FROM __sys__"; if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Running SQL ["+sql+"]"); } final JsArrayMixed args = JsArrayMixed.createArray().cast(); tx.executeSQL(sql, args, new SQLTransaction.SQLStatementCallback() { @Override public void onSuccess(SQLTransaction tx, SQLResultSet rs) { objectStoreNames = CollectionFactory.createArray(); int length = rs.getRows().length(); for (int i = 0; i < length; i++) { String objectStoreName = JsUtils.readStringPropertyValue(rs.getRows().itemObject(i), "name"); objectStoreNames.add(objectStoreName); } callback.onSuccess(); } },new SQLTransaction.SQLStatementErrorCallback() { @Override public boolean onError(SQLTransaction tx, SQLError error) { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.SEVERE, messages.databaseOpenError(getName(), error.getName()+"-"+error.getMessage())); } throw new DatabaseException(messages.databaseOpenError(getName(), error.getName()+"-"+error.getMessage())); } }); } }, new SQLDatabase.SQLTransactionErrorCallback() { @Override public void onError(SQLError error) { throw new DatabaseException("Error loading object store names. Error ["+error.getName()+"]. Message ["+error.getMessage()+"]"); } }, null); } private void updateDBVersionAndOpen(final int oldVersion, final DatabaseCallback callback, final SQLTransaction.SQLStatementErrorCallback errorCallback) { systemDatabase.transaction(new SQLDatabase.SQLTransactionCallback() { @Override public void onTransaction(SQLTransaction tx) { String sql = "UPDATE dbVersions SET version = ? WHERE name = ?"; if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Running SQL ["+sql+"]"); } JsArrayMixed args = JsArrayMixed.createArray().cast(); args.push(version); args.push(name); tx.executeSQL(sql, args, new SQLTransaction.SQLStatementCallback() { @Override public void onSuccess(SQLTransaction tx, SQLResultSet rs) { database.transaction(new SQLDatabase.SQLTransactionCallback() { @Override public void onTransaction(SQLTransaction tx) { try { updateDatabaseStructure(tx, callback); } catch (RuntimeException e) { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.SEVERE, messages.databaseUpgradeError(getName(), e.getMessage()), e); } throw e; } } }, new SQLDatabase.SQLTransactionErrorCallback() { @Override public void onError(SQLError error) { String message = messages.databaseUpgradeError(getName(), error.getMessage()); if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.SEVERE, message); } throw new DatabaseException(message); } }, new SQLDatabase.SQLCallback() { @Override public void onSuccess() { opened = true; callback.onSuccess(); } }); } }, errorCallback); } }, null, null); } protected void doDelete(final DatabaseCallback callback) { final SQLTransaction.SQLStatementErrorCallback errorCallback = getDeleteErrorHandler(callback); doDelete(callback, errorCallback); } protected SQLStatementErrorCallback getDeleteErrorHandler(final DatabaseCallback callback) { return new SQLTransaction.SQLStatementErrorCallback() { @Override public boolean onError(SQLTransaction tx, SQLError error) { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.SEVERE, messages.databaseDeleteError(getName(), error.getName()+"-"+error.getMessage())); } if (callback != null) { callback.onError(messages.databaseDeleteError(getName(), error.getName()+"-"+error.getMessage())); } else if (errorHandler != null) { errorHandler.onError(messages.databaseDeleteError(getName(), error.getName()+"-"+error.getMessage())); } return true; } }; } protected void doDelete(final DatabaseCallback callback, final SQLTransaction.SQLStatementErrorCallback errorCallback) { systemDatabase.transaction(new SQLDatabase.SQLTransactionCallback() { @Override public void onTransaction(SQLTransaction tx) { String sql = "SELECT * FROM dbVersions WHERE name = ?"; if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Running SQL ["+sql+"]"); } final JsArrayMixed args = JsArrayMixed.createArray().cast(); args.push(name); tx.executeSQL(sql, args, new SQLTransaction.SQLStatementCallback() { @Override public void onSuccess(SQLTransaction tx, SQLResultSet rs) { if (rs.getRows().length() == 0) { if (callback != null) { callback.onSuccess(); } return; } deleteDBTables(callback, errorCallback); } }, errorCallback); } }, null, null); } protected void deleteDBTables(SQLTransaction tx, final DatabaseCallback callback, final SQLStatementErrorCallback errorCallback, final boolean removeSystemTables) { String sql = "SELECT * FROM __sys__"; if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Running SQL ["+sql+"]"); } final JsArrayMixed args = JsArrayMixed.createArray().cast(); tx.executeSQL(sql, args, new SQLTransaction.SQLStatementCallback() { @Override public void onSuccess(SQLTransaction tx, SQLResultSet rs) { int length = rs.getRows().length(); String sql; final JsArrayMixed args = JsArrayMixed.createArray().cast(); for (int i=0; i< length; i++) { sql = "DROP TABLE "+JsUtils.readStringPropertyValue(rs.getRows().itemObject(i), "name"); if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Running SQL ["+sql+"]"); } tx.executeSQL(sql, args, null, errorCallback); } if (removeSystemTables) { sql = "DROP TABLE __sys__"; 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) { deleteFromDbVersions(callback, errorCallback); } }, errorCallback); } else { callback.onSuccess(); } } }, errorCallback); } protected void deleteDBTables(final DatabaseCallback callback, final SQLStatementErrorCallback errorCallback) { final SQLDatabase database = SQLDatabaseFactory.openDatabase(name, name, DEFAULT_DB_SIZE); database.transaction(new SQLDatabase.SQLTransactionCallback() { @Override public void onTransaction(final SQLTransaction tx) { deleteDBTables(tx, callback, errorCallback, true); } }, null, null); } protected void deleteFromDbVersions(final DatabaseCallback callback, final SQLStatementErrorCallback errorCallback) { systemDatabase.transaction(new SQLDatabase.SQLTransactionCallback() { @Override public void onTransaction(SQLTransaction tx) { String sql = "DELETE FROM dbVersions WHERE name = ? "; if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Running SQL ["+sql+"]"); } final JsArrayMixed args = JsArrayMixed.createArray().cast(); args.push(name); tx.executeSQL(sql, args, new SQLTransaction.SQLStatementCallback() { @Override public void onSuccess(SQLTransaction tx, SQLResultSet rs) { callback.onSuccess(); } }, errorCallback); } }, null, null); } protected void createFileStore(SQLTransaction tx) { final SQLTransaction.SQLStatementErrorCallback errorCallback = new SQLTransaction.SQLStatementErrorCallback() { @Override public boolean onError(SQLTransaction tx, SQLError error) { String message = messages.objectStoreOperationError(error.getName() + " - " + error.getMessage()); if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.SEVERE, message); } else if (errorHandler != null) { errorHandler.onError(message); } return true; } }; String sql = "INSERT INTO __sys__(name) VALUES (?)"; JsArrayMixed args = JsArrayMixed.createArray().cast(); args.push(FileStore.OBJECT_STORE_NAME); tx.executeSQL(sql, args, new SQLTransaction.SQLStatementCallback() { @Override public void onSuccess(SQLTransaction tx, SQLResultSet rs) { String sql = "CREATE TABLE \""+FileStore.OBJECT_STORE_NAME+"\" (value BLOB, fileName TEXT PRIMARY KEY)"; JsArrayMixed args = JsArrayMixed.createArray().cast(); tx.executeSQL(sql, args, null, errorCallback); } }, errorCallback); } protected abstract void updateDatabaseStructure(SQLTransaction tx, DatabaseCallback callback); protected abstract <K, V> WSQLAbstractObjectStore<K, V> getObjectStore(String storeName, WSQLTransaction transaction); static interface SQLCallback { void onSuccess(); } }