package org.commcare.models; import net.sqlcipher.database.SQLiteDatabase; import org.commcare.CommCareApplication; import org.commcare.cases.entity.Entity; import org.commcare.logging.XPathErrorLogger; import org.commcare.models.database.user.models.EntityStorageCache; import org.commcare.suite.model.DetailField; import org.commcare.suite.model.Text; import org.commcare.utils.SessionUnavailableException; import org.commcare.utils.StringUtils; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.instance.TreeReference; import org.javarosa.xpath.XPathException; import org.javarosa.xpath.expr.FunctionUtils; import org.javarosa.xpath.expr.XPathExpression; import org.javarosa.xpath.parser.XPathSyntaxException; import java.util.Enumeration; import java.util.Hashtable; /** * An AsyncEntity is an entity reference which is capable of building its * values (evaluating all Text elements/background data elements) lazily * rather than upfront when the entity is constructed. * * It is threadsafe. * * It will attempt to Cache its values persistently by a derived entity key rather * than evaluating them each time when possible. This can be slow to perform across * all entities internally due to the overhead of establishing the db connection, it * is recommended that the entities be primed externally with a bulk query. * * @author ctsims */ public class AsyncEntity extends Entity<TreeReference> { private final DetailField[] fields; private final Object[] data; private final String[] sortData; private final boolean[] relevancyData; private final String[][] sortDataPieces; private final EvaluationContext context; private final Hashtable<String, XPathExpression> mVariableDeclarations; private boolean mVariableContextLoaded = false; private final String mCacheIndex; private final String mDetailId; private final EntityStorageCache mEntityStorageCache; /* * the Object's lock. NOTE: _DO NOT LOCK ANY CODE WHICH READS/WRITES THE CACHE * UNTIL YOU HAVE A LOCK FOR THE DB! * * The lock is for the integrity of this object, not the larger environment, * and any DB access has its own implict lock between threads, so it's easy * to accidentally deadlock if you don't already have the db lock * * Basically you should never be calling mEntityStorageCache from inside of * a lock that */ private final Object mAsyncLock = new Object(); public AsyncEntity(DetailField[] fields, EvaluationContext ec, TreeReference t, Hashtable<String, XPathExpression> variables, EntityStorageCache cache, String cacheIndex, String detailId, String extraKey) { super(t, extraKey); this.fields = fields; this.data = new Object[fields.length]; this.sortData = new String[fields.length]; this.sortDataPieces = new String[fields.length][]; this.relevancyData = new boolean[fields.length]; this.context = ec; this.mVariableDeclarations = variables; this.mEntityStorageCache = cache; //TODO: It's weird that we pass this in, kind of, but the thing is that we don't want to figure out //if this ref is _cachable_ every time, since it's a pretty big lift this.mCacheIndex = cacheIndex; this.mDetailId = detailId; } private void loadVariableContext() { synchronized (mAsyncLock) { if (!mVariableContextLoaded) { //These are actually in an ordered hashtable, so we can't just get the keyset, since it's //in a 1.3 hashtable equivalent for (Enumeration<String> en = mVariableDeclarations.keys(); en.hasMoreElements(); ) { String key = en.nextElement(); context.setVariable(key, FunctionUtils.unpack(mVariableDeclarations.get(key).eval(context))); } mVariableContextLoaded = true; } } } @Override public Object getField(int i) { synchronized (mAsyncLock) { loadVariableContext(); if (data[i] == null) { try { data[i] = fields[i].getTemplate().evaluate(context); } catch (XPathException xpe) { XPathErrorLogger.INSTANCE.logErrorToCurrentApp(xpe); xpe.printStackTrace(); data[i] = "<invalid xpath: " + xpe.getMessage() + ">"; } } return data[i]; } } @Override public String getNormalizedField(int i) { String normalized = this.getSortField(i); if (normalized == null) { return ""; } return normalized; } @Override public String getSortField(int i) { //Get a db handle so we can get an outer lock SQLiteDatabase db; try { db = CommCareApplication.instance().getUserDbHandle(); } catch (SessionUnavailableException e) { return null; } //get the db lock db.beginTransaction(); try { //get our second lock. synchronized (mAsyncLock) { if (sortData[i] == null) { // sort data not in search field cache; load and store it Text sortText = fields[i].getSort(); if (sortText == null) { db.setTransactionSuccessful(); return null; } String cacheKey = AsyncNodeEntityFactory.getCacheKey(mDetailId, String.valueOf(i)); if (mCacheIndex != null) { //Check the cache! String value = mEntityStorageCache.retrieveCacheValue(mCacheIndex, cacheKey); if (value != null) { this.setSortData(i, value); db.setTransactionSuccessful(); return sortData[i]; } } loadVariableContext(); try { sortText = fields[i].getSort(); if (sortText == null) { this.setSortData(i, getFieldString(i)); } else { this.setSortData(i, StringUtils.normalize(sortText.evaluate(context))); } mEntityStorageCache.cache(mCacheIndex, cacheKey, sortData[i]); } catch (XPathException xpe) { XPathErrorLogger.INSTANCE.logErrorToCurrentApp(xpe); xpe.printStackTrace(); sortData[i] = "<invalid xpath: " + xpe.getMessage() + ">"; } } db.setTransactionSuccessful(); return sortData[i]; } } finally { //free the db lock. db.endTransaction(); } } @Override public int getNumFields() { return fields.length; } @Override public boolean isValidField(int fieldIndex) { //NOTE: This totally jacks the asynchronicity. It's only used in //detail fields for now, so not super important, but worth bearing //in mind synchronized (mAsyncLock) { loadVariableContext(); if (getField(fieldIndex).equals("")) { return false; } try { this.relevancyData[fieldIndex] = this.fields[fieldIndex].isRelevant(this.context); } catch (XPathSyntaxException e) { final String msg = "Invalid relevant condition for field : " + fields[fieldIndex].getHeader().toString(); XPathErrorLogger.INSTANCE.logErrorToCurrentApp(msg); throw new RuntimeException(msg); } return this.relevancyData[fieldIndex]; } } @Override public Object[] getData() { for (int i = 0; i < this.getNumFields(); ++i) { this.getField(i); } return data; } @Override public String[] getSortFieldPieces(int i) { if (getSortField(i) == null) { return new String[0]; } return sortDataPieces[i]; } private void setSortData(int i, String val) { synchronized (mAsyncLock) { this.sortData[i] = val; this.sortDataPieces[i] = breakUpField(val); } } public void setSortData(String cacheKey, String val) { int sortIndex = EntityStorageCache.getSortFieldIdFromCacheKey(mDetailId, cacheKey); if (sortIndex != -1) { setSortData(sortIndex, val); } } private static String[] breakUpField(String input) { if (input == null) { return new String[0]; } else { //We always fuzzy match on the sort field and only if it is available //(as a way to restrict possible matching) return input.split("\\s+"); } } }