/*
* 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.db.Transaction.Mode;
import org.cruxframework.crux.core.client.db.WSQLAbstractObjectStore.EncodeCallback;
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 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
* @param <K> The type of the key used to identify objects into the cursor.
* @param <V> The type of the objects referenced by this cursor
*
*/
public abstract class WSQLCursor<K, P, V> extends DBObject implements Cursor<K, V>
{
private static final int NOT_INITIALIZED = -1;
private static final int CURSOR_BEGIN = 0;
protected final org.cruxframework.crux.core.client.db.Cursor.CursorDirection direction;
protected final WSQLTransaction transaction;
protected WSQLKeyRange<K> keyRange;
protected String objectStoreName;
protected int offset;
protected int length;
protected SQLResultSet resultSet;
protected DatabaseCursorCallback<K, V> callback;
protected Array<String> keyPath;
protected Array<String> indexColumnNames;
protected K cursorKey;
protected final boolean autoIncrement;
protected WSQLCursor(WSQLAbstractDatabase db, WSQLKeyRange<K> range, String objectStoreName, boolean autoIncrement, CursorDirection direction, WSQLTransaction transaction)
{
super(db);
this.keyRange = range;
this.objectStoreName = objectStoreName;
this.autoIncrement = autoIncrement;
this.direction = direction;
this.transaction = transaction;
this.offset = NOT_INITIALIZED;
this.length = NOT_INITIALIZED;
this.keyPath = getKeyPath();
this.indexColumnNames = getIndexedColumnNames();
}
public void start(final DatabaseCursorCallback<K, V> c)
{
transaction.addRequest(new WSQLTransaction.RequestOperation()
{
@Override
public void doOperation(final SQLTransaction tx)
{
StringBuilder sql = new StringBuilder("SELECT * FROM \"").append(objectStoreName).append("\"");
JsArrayMixed args = JsArrayMixed.createArray().cast();
if ((keyRange != null) || (cursorKey != null))
{
sql.append(" WHERE ");
}
if (keyRange != null)
{
addKeyRangeToQuery(keyRange, sql, args);
}
if (cursorKey != null)
{
if (keyRange != null)
{
sql.append(" AND ");
}
addKeyToQuery(cursorKey, sql, args);
}
if (getDirection().equals(CursorDirection.nextunique) || getDirection().equals(CursorDirection.prevunique))
{
sql.append(" GROUP BY ");
appendGroupColumns(sql);
}
sql.append(" ORDER BY ");
appendGroupColumns(sql);
if (getDirection().equals(CursorDirection.prev) || getDirection().equals(CursorDirection.prevunique))
{
sql.append(" DESC");
}
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)
{
resultSet = rs;
offset = CURSOR_BEGIN;
length = rs.getRows().length();
callback = c;
fireSuccess();
}
}, getErrorHandler(c));
}
}, new Mode[]{Mode.readOnly, Mode.readWrite});
}
@Override
public void advance(int count)
{
if (offset == NOT_INITIALIZED)
{
throw new DatabaseException("Cursor is not initialized. Object store ["+objectStoreName+"]");
}
if (count <= 0)
{
throw new DatabaseException("Count can not be 0 or negative. Object store ["+objectStoreName+"]");
}
offset += count;
if (LogConfiguration.loggingIsEnabled())
{
if (offset >= length)
{
logger.log(Level.FINE, "Reached the end of cursor");
}
}
fireSuccess();
};
@Override
public void continueCursor()
{
continueCursor(null);
}
public void continueCursor(K key)
{
this.cursorKey = key;
if (offset == NOT_INITIALIZED || key != null)
{
if (offset != NOT_INITIALIZED)
{
offset = NOT_INITIALIZED;
length = NOT_INITIALIZED;
resultSet = null;
}
start(callback);
}
else
{
offset++;
if (LogConfiguration.loggingIsEnabled())
{
if (offset == length)
{
logger.log(Level.FINE, "Reached the end of cursor");
}
}
fireSuccess();
}
}
@Override
public void delete()
{
if (offset == NOT_INITIALIZED)
{
throw new DatabaseException("Cursor is not initialized. Object store ["+objectStoreName+"]");
}
if (offset >= length)
{
throw new DatabaseException("Can not update cursors. It is out of range. Object store ["+objectStoreName+"]");
}
transaction.addRequest(new WSQLTransaction.RequestOperation()
{
@Override
public void doOperation(SQLTransaction tx)
{
StringBuilder sql = new StringBuilder("DELETE FROM \"").append(objectStoreName).append("\"");
final JsArrayMixed args = JsArrayMixed.createArray().cast();
sql.append(" WHERE ");
addPrimaryKeyToQuery(getPrimaryKey(), 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 (rs.getRowsAffected() == 1)
{
fireSuccess();
}
else
{
callback.onError("No rowns with key found");
transaction.abort();
}
}
}, getErrorHandler(callback));
}
}, new Mode[]{Mode.readWrite});
}
public void update(V value)
{
if (offset == NOT_INITIALIZED)
{
throw new DatabaseException("Cursor is not initialized. Object store ["+objectStoreName+"]");
}
if (offset >= length)
{
throw new DatabaseException("Can not update cursors. It is out of range. Object store ["+objectStoreName+"]");
}
encodeObject(value, new EncodeCallback()
{
@Override
public void onEncode(final JSONObject encoded)
{
transaction.addRequest(new WSQLTransaction.RequestOperation()
{
@Override
public void doOperation(SQLTransaction tx)
{
StringBuilder sql = new StringBuilder("UPDATE \"").append(objectStoreName).append("\" SET ");
final JsArrayMixed args = JsArrayMixed.createArray().cast();
args.push(encoded.toString());
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 = ? WHERE ");
addPrimaryKeyToQuery(getPrimaryKey(), 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 (rs.getRowsAffected() == 1)
{
fireSuccess();
}
else
{
callback.onError("No rowns with key found");
transaction.abort();
}
}
}, getErrorHandler(callback));
}
}, new Mode[]{Mode.readWrite});
}
});
}
@Override
public boolean hasValue()
{
return getValue() != null;
}
protected int size()
{
return length;
}
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 appendGroupColumns(StringBuilder sql)
{
for (int i=0; i< keyPath.size(); i++)
{
if (i > 0)
{
sql.append(", ");
}
sql.append("\""+keyPath.get(i)+"\"");
}
}
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;
}
};
}
@Override
public CursorDirection getDirection()
{
return direction;
}
public V getValue()
{
if (offset == NOT_INITIALIZED)
{
throw new DatabaseException("Cursor is not initialized. Object store ["+objectStoreName+"]");
}
if (offset < length)
{
V object = decodeObject(JsUtils.readStringPropertyValue(resultSet.getRows().itemObject(offset), "value"));
if (autoIncrement)
{
setObjectKey(object, getPrimaryKey());
}
return object;
}
else
{
return null;
}
}
public JsArrayMixed getNativeArrayKey()
{
if (offset == NOT_INITIALIZED)
{
throw new DatabaseException("Cursor is not initialized. Object store ["+objectStoreName+"]");
}
if (offset < length && keyPath != null && keyPath.size() > 0)
{
JsArrayMixed out = JsArrayMixed.createArray().cast();
JavaScriptObject object = resultSet.getRows().itemObject(offset);
for (int i = 0; i< keyPath.size(); i++)
{
JsUtils.readPropertyValue(object, keyPath.get(i), out, true);
}
return out;
}
return null;
}
protected void fireSuccess()
{
delayCallbackSuccessCall(this);
}
private native void delayCallbackSuccessCall(WSQLCursor<K, P, V> cursor)/*-{
setTimeout(function(){
cursor.@org.cruxframework.crux.core.client.db.WSQLCursor::callCallbackSuccess()();
}, 0);
}-*/;
private void callCallbackSuccess()
{
callback.onSuccess((offset <= length)?this:null);
}
protected abstract void setObjectKey(V object, P key);
protected abstract Array<String> getIndexedColumnNames();
protected abstract P getPrimaryKey();
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 void addPrimaryKeyToQuery(final P key, StringBuilder sql, JsArrayMixed args);
protected abstract void encodeObject(V object, EncodeCallback callback);
protected abstract V decodeObject(String encodedObject);
}