package org.commcare.models; import android.util.Log; import net.sqlcipher.Cursor; import net.sqlcipher.database.SQLiteDatabase; import org.commcare.CommCareApplication; import org.commcare.cases.entity.Entity; import org.commcare.cases.entity.NodeEntityFactory; import org.commcare.models.database.AndroidTableBuilder; import org.commcare.models.database.DbUtil; import org.commcare.models.database.SqlStorage; import org.commcare.models.database.user.models.EntityStorageCache; import org.commcare.suite.model.Detail; import org.commcare.suite.model.DetailField; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.instance.TreeReference; import org.javarosa.core.model.utils.CacheHost; import org.javarosa.core.util.OrderedHashtable; import org.javarosa.xpath.expr.XPathExpression; import java.util.Hashtable; import java.util.Vector; /** * @author ctsims */ public class AsyncNodeEntityFactory extends NodeEntityFactory { private static final String TAG = AsyncNodeEntityFactory.class.getSimpleName(); private final OrderedHashtable<String, XPathExpression> mVariableDeclarations; private final Hashtable<String, AsyncEntity> mEntitySet = new Hashtable<>(); private final EntityStorageCache mEntityCache; private CacheHost mCacheHost = null; private Boolean mTemplateIsCachable = null; private static final Object mAsyncLock = new Object(); private Thread mAsyncPrimingThread; public AsyncNodeEntityFactory(Detail d, EvaluationContext ec) { super(d, ec); mVariableDeclarations = detail.getVariableDeclarations(); mEntityCache = new EntityStorageCache("case"); } @Override public Entity<TreeReference> getEntity(TreeReference data) { EvaluationContext nodeContext = new EvaluationContext(ec, data); mCacheHost = nodeContext.getCacheHost(data); String mCacheIndex = null; if (mTemplateIsCachable == null) { mTemplateIsCachable = mCacheHost != null && mCacheHost.isReferencePatternCachable(data); } if (mTemplateIsCachable) { if (mCacheHost == null) { Log.d(TAG, "Template is cachable, but there's no cache host for this instance?"); } else { mCacheIndex = mCacheHost.getCacheIndex(data); } } String entityKey = loadCalloutDataMapKey(nodeContext); AsyncEntity entity = new AsyncEntity(detail.getFields(), nodeContext, data, mVariableDeclarations, mEntityCache, mCacheIndex, detail.getId(), entityKey); if (mCacheIndex != null) { mEntitySet.put(mCacheIndex, entity); } return entity; } /** * Bulk loads search field cache from db. * Note that the cache is lazily built upon first case list search. */ private void primeCache() { if (mTemplateIsCachable == null || !mTemplateIsCachable || mCacheHost == null) { return; } String[][] cachePrimeKeys = mCacheHost.getCachePrimeGuess(); if (cachePrimeKeys == null) { return; } Vector<Integer> sortKeys = new Vector<>(); String validKeys = buildValidKeys(sortKeys, detail.getFields()); if ("".equals(validKeys)) { return; } //Create our full args tree. We need the elements from the cache primer //along with the specific keys we wanna pull out String[] args = new String[cachePrimeKeys[1].length + sortKeys.size()]; System.arraycopy(cachePrimeKeys[1], 0, args, 0, cachePrimeKeys[1].length); for (int i = 0; i < sortKeys.size(); ++i) { args[cachePrimeKeys[1].length + i] = getCacheKey(detail.getId(), String.valueOf(sortKeys.get(i))); } String[] names = cachePrimeKeys[0]; String whereClause = buildKeyNameWhereClause(names); long now = System.currentTimeMillis(); SQLiteDatabase db = CommCareApplication.instance().getUserDbHandle(); String sqlStatement = "SELECT entity_key, cache_key, value FROM entity_cache JOIN AndroidCase ON entity_cache.entity_key = AndroidCase.commcare_sql_id WHERE " + whereClause + " AND cache_key IN " + validKeys; if (SqlStorage.STORAGE_OUTPUT_DEBUG) { DbUtil.explainSql(db, sqlStatement, args); } populateEntitySet(db, sqlStatement, args); if (SqlStorage.STORAGE_OUTPUT_DEBUG) { Log.d(TAG, "Sequential Cache Load: " + (System.currentTimeMillis() - now) + "ms"); } } private String buildValidKeys(Vector<Integer> sortKeys, DetailField[] fields) { String validKeys = "("; boolean added = false; for (int i = 0; i < fields.length; ++i) { //We're only gonna pull out the fields we can index/sort on if (fields[i].getSort() != null) { sortKeys.add(i); validKeys += "?, "; added = true; } } if (added) { return validKeys.substring(0, validKeys.length() - 2) + ")"; } else { return ""; } } public static String getCacheKey(String detailId, String mFieldId) { return detailId + "_" + mFieldId; } private String buildKeyNameWhereClause(String[] names) { String whereClause = ""; for (int i = 0; i < names.length; ++i) { whereClause += AndroidTableBuilder.scrubName(names[i]) + " = ?"; if (i + 1 < names.length) { whereClause += " AND "; } } return whereClause; } private void populateEntitySet(SQLiteDatabase db, String sqlStatement, String[] args) { //TODO: This will _only_ query up to about a meg of data, which is an un-great limitation. //Should probably split this up SQL LIMIT based looped //For reference the current limitation is about 10k rows with 1 field each. Cursor walker = db.rawQuery(sqlStatement, args); while (walker.moveToNext()) { String entityId = walker.getString(walker.getColumnIndex("entity_key")); String cacheId = walker.getString(walker.getColumnIndex("cache_key")); String val = walker.getString(walker.getColumnIndex("value")); if (this.mEntitySet.containsKey(entityId)) { this.mEntitySet.get(entityId).setSortData(cacheId, val); } } walker.close(); } @Override protected void prepareEntitiesInternal() { synchronized (mAsyncLock) { if (mAsyncPrimingThread == null) { mAsyncPrimingThread = new Thread(new Runnable() { @Override public void run() { primeCache(); } }); mAsyncPrimingThread.start(); } } } @Override protected boolean isEntitySetReadyInternal() { synchronized (mAsyncLock) { return mAsyncPrimingThread == null || !mAsyncPrimingThread.isAlive(); } } }