package org.commcare.android.util; import net.sqlcipher.database.SQLiteDatabase; import org.commcare.CommCareApplication; import org.commcare.CommCareTestApplication; import org.commcare.cases.ledger.Ledger; import org.commcare.data.xml.DataModelPullParser; import org.commcare.data.xml.TransactionParser; import org.commcare.data.xml.TransactionParserFactory; import org.commcare.engine.cases.AndroidCaseInstanceTreeElement; import org.commcare.engine.cases.AndroidLedgerInstanceTreeElement; import org.commcare.models.AndroidClassHasher; import org.commcare.models.database.ConcreteAndroidDbHelper; import org.commcare.models.database.AndroidPrototypeFactorySetup; import org.commcare.models.database.SqlStorage; import org.commcare.models.database.user.DatabaseUserOpenHelper; import org.commcare.android.database.user.models.ACase; import org.commcare.models.database.user.models.AndroidCaseIndexTable; import org.commcare.models.database.user.models.EntityStorageCache; import org.commcare.network.HttpRequestEndpointsMock; import org.commcare.test.utilities.CaseTestUtils; import org.commcare.utils.AndroidInstanceInitializer; import org.commcare.utils.FormSaveUtil; import org.commcare.utils.GlobalConstants; import org.commcare.xml.AndroidBulkCaseXmlParser; import org.commcare.xml.AndroidCaseXmlParser; import org.commcare.xml.AndroidTransactionParserFactory; import org.commcare.xml.CaseXmlParser; import org.commcare.xml.FormInstanceXmlParser; import org.commcare.xml.LedgerXmlParsers; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.instance.AbstractTreeElement; import org.javarosa.core.model.instance.DataInstance; import org.javarosa.core.model.instance.ExternalDataInstance; import org.javarosa.core.model.instance.TreeReference; import org.javarosa.core.util.externalizable.PrototypeFactory; import org.javarosa.xml.util.InvalidStructureException; import org.javarosa.xml.util.UnfullfilledRequirementsException; import org.kxml2.io.KXmlParser; import org.robolectric.RuntimeEnvironment; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.io.InputStream; import java.util.Collections; import java.util.Hashtable; /** * @author ctsims */ public class TestUtils { //TODO: Move this to the application or somewhere better static /** * Initialize all of the static hooks we need to make storage possible * in the mocked/shadow world */ public static void initializeStaticTestStorage() { //Sets the static strategy for the deserializtion code to be //based on an optimized md5 hasher. Major speed improvements. AndroidPrototypeFactorySetup.setDBUtilsPrototypeFactory(new LivePrototypeFactory(AndroidClassHasher.getInstance())); disableSqlOptimizations(); } public static void disableSqlOptimizations() { // For now, disable the optimizations, since they require in-depth SQL code that // we need better shadows for SqlStorage.STORAGE_OPTIMIZATIONS_ACTIVE = false; } /** * Get a form instance and case enabled parsing factory */ private static TransactionParserFactory getFactory(final SQLiteDatabase db) { return getFactory(db, false); } /** * Get a form instance and case enabled parsing factory */ private static TransactionParserFactory getFactory(final SQLiteDatabase db, final boolean bulkProcessingEnabled) { final Hashtable<String, String> formInstanceNamespaces; if (CommCareApplication.instance().getCurrentApp() != null) { formInstanceNamespaces = FormSaveUtil.getNamespaceToFilePathMap(CommCareApplication.instance()); } else { formInstanceNamespaces = null; } return new TransactionParserFactory() { @Override public TransactionParser getParser(KXmlParser parser) { String namespace = parser.getNamespace(); if (namespace != null && formInstanceNamespaces != null && formInstanceNamespaces.containsKey(namespace)) { return new FormInstanceXmlParser(parser, CommCareApplication.instance(), Collections.unmodifiableMap(formInstanceNamespaces), CommCareApplication.instance().getCurrentApp().fsPath(GlobalConstants.FILE_CC_FORMS)); } else if(CaseXmlParser.CASE_XML_NAMESPACE.equals(parser.getNamespace()) && "case".equalsIgnoreCase(parser.getName())) { //Note - this isn't even actually bulk processing. since this class is static //there's no good lifecycle to manage the bulk processor in, but at least //this will validate that the bulk processor works. if(bulkProcessingEnabled) { return new AndroidBulkCaseXmlParser(parser, getCaseStorage(db), new EntityStorageCache("case", db), new AndroidCaseIndexTable(db), new HttpRequestEndpointsMock()) { @Override protected SQLiteDatabase getDbHandle() { return db; } }; } else { return new AndroidCaseXmlParser(parser, getCaseStorage(db), new EntityStorageCache("case", db), new AndroidCaseIndexTable(db)) { @Override protected SQLiteDatabase getDbHandle() { return db; } }; } } else if (LedgerXmlParsers.STOCK_XML_NAMESPACE.equals(namespace)) { return new LedgerXmlParsers(parser, getLedgerStorage(db)); } return null; } }; } /** * Process an input XML file for transactions and update the relevant databases. */ public static void processResourceTransaction(String resourcePath) { processResourceTransaction(resourcePath, false); } /** * Process an input XML file for transactions and update the relevant databases. */ public static void processResourceTransaction(String resourcePath, boolean bulkProcessingEnabled) { final SQLiteDatabase db = getTestDb(); DataModelPullParser parser; try{ InputStream is = System.class.getResourceAsStream(resourcePath); parser = new DataModelPullParser(is, getFactory(db, bulkProcessingEnabled), true, true); parser.parse(); is.close(); } catch(IOException ioe) { throw wrapError(ioe, "IO Error parsing transactions"); } catch (InvalidStructureException e) { throw wrapError(e, "Bad Transaction"); } catch (XmlPullParserException e) { throw wrapError(e, "Bad XML"); } catch (UnfullfilledRequirementsException e) { throw wrapError(e, "Bad State"); } } public static void processResourceTransactionIntoAppDb(String resourcePath) { DataModelPullParser parser; AndroidTransactionParserFactory androidTransactionFactory = new AndroidTransactionParserFactory(CommCareApplication.instance().getApplicationContext(), null); if (CommCareApplication.instance().getCurrentApp() != null) { Hashtable<String, String> formInstanceNamespaces = FormSaveUtil.getNamespaceToFilePathMap(CommCareApplication.instance()); androidTransactionFactory.initFormInstanceParser(formInstanceNamespaces); } try{ InputStream is = System.class.getResourceAsStream(resourcePath); parser = new DataModelPullParser(is, androidTransactionFactory, true, true); parser.parse(); is.close(); } catch(IOException ioe) { throw wrapError(ioe, "IO Error parsing transactions"); } catch (InvalidStructureException e) { throw wrapError(e, "Bad Transaction"); } catch (XmlPullParserException e) { throw wrapError(e, "Bad XML"); } catch (UnfullfilledRequirementsException e) { throw wrapError(e, "Bad State"); } } /** * @return The hook for the test user-db */ private static SQLiteDatabase getTestDb() { DatabaseUserOpenHelper helper = new DatabaseUserOpenHelper(RuntimeEnvironment.application, "Test"); return helper.getWritableDatabase("Test"); } public static PrototypeFactory getStaticPrototypeFactory(){ return CommCareTestApplication.instance().getPrototypeFactory(RuntimeEnvironment.application); } /** * @return A test-db case storage object */ public static SqlStorage<ACase> getCaseStorage() { return getCaseStorage(getTestDb()); } /** * @return The case storage object for the provided db */ private static SqlStorage<ACase> getCaseStorage(SQLiteDatabase db) { return new SqlStorage<>(ACase.STORAGE_KEY, ACase.class, new ConcreteAndroidDbHelper(RuntimeEnvironment.application, db) { @Override public PrototypeFactory getPrototypeFactory() { return getStaticPrototypeFactory(); } }); } private static SqlStorage<Ledger> getLedgerStorage(SQLiteDatabase db) { return new SqlStorage<>(Ledger.STORAGE_KEY, Ledger.class, new ConcreteAndroidDbHelper(RuntimeEnvironment.application, db) { @Override public PrototypeFactory getPrototypeFactory() { return getStaticPrototypeFactory(); } }); } //TODO: Make this work natively with the CommCare Android IIF /** * @return An evaluation context which is capable of evaluating against * the connected storage instances: casedb is the only one supported for now */ public static EvaluationContext getEvaluationContextWithoutSession() { final SQLiteDatabase db = getTestDb(); AndroidInstanceInitializer iif = new AndroidInstanceInitializer() { @Override public AbstractTreeElement setupCaseData(ExternalDataInstance instance) { SqlStorage<ACase> storage = getCaseStorage(db); AndroidCaseInstanceTreeElement casebase = new AndroidCaseInstanceTreeElement(instance.getBase(), storage, new AndroidCaseIndexTable(db)); instance.setCacheHost(casebase); return casebase; } @Override protected AbstractTreeElement setupLedgerData(ExternalDataInstance instance) { SqlStorage<Ledger> storage = getLedgerStorage(db); return new AndroidLedgerInstanceTreeElement(instance.getBase(), storage); } }; return buildEvaluationContext(iif); } public static EvaluationContext getEvaluationContextWithAndroidIIF() { AndroidInstanceInitializer iif = new AndroidInstanceInitializer(CommCareApplication.instance().getCurrentSession()); return buildEvaluationContext(iif); } private static EvaluationContext buildEvaluationContext(AndroidInstanceInitializer iif) { ExternalDataInstance edi = new ExternalDataInstance(CaseTestUtils.CASE_INSTANCE, "casedb"); DataInstance specializedDataInstance = edi.initialize(iif, "casedb"); ExternalDataInstance ledgerDataInstanceRaw = new ExternalDataInstance(CaseTestUtils.LEDGER_INSTANCE, "ledgerdb"); DataInstance ledgerDataInstance = ledgerDataInstanceRaw.initialize(iif, "ledger"); Hashtable<String, DataInstance> formInstances = new Hashtable<>(); formInstances.put("casedb", specializedDataInstance); formInstances.put("ledger", ledgerDataInstance); return new EvaluationContext(new EvaluationContext(null), formInstances, TreeReference.rootRef()); } public static RuntimeException wrapError(Exception e, String prefix) { e.printStackTrace(); RuntimeException re = new RuntimeException(prefix + ": " + e.getMessage()); re.initCause(e); throw re; } }