<commit_before>package org.commcare.android.tasks;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.Hashtable;
import java.util.Vector;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;

import org.commcare.android.database.SqlStorage;
import org.commcare.android.database.user.models.ACase;
import org.commcare.android.database.user.models.FormRecord;
import org.commcare.android.database.user.models.SessionStateDescriptor;
import org.commcare.android.javarosa.AndroidLogger;
import org.commcare.android.models.AndroidSessionWrapper;
import org.commcare.android.tasks.templates.CommCareTask;
import org.commcare.android.util.FileUtil;
import org.commcare.android.util.SessionUnavailableException;
import org.commcare.cases.model.Case;
import org.commcare.dalvik.application.CommCareApplication;
import org.commcare.dalvik.odk.provider.InstanceProviderAPI.InstanceColumns;
import org.commcare.data.xml.DataModelPullParser;
import org.commcare.data.xml.TransactionParser;
import org.commcare.data.xml.TransactionParserFactory;
import org.commcare.util.CommCarePlatform;
import org.commcare.xml.AndroidCaseXmlParser;
import org.commcare.xml.BestEffortBlockParser;
import org.commcare.xml.CaseXmlParser;
import org.commcare.xml.MetaDataXmlParser;
import org.javarosa.xml.util.InvalidStructureException;
import org.javarosa.xml.util.UnfullfilledRequirementsException;
import org.javarosa.core.model.utils.DateUtils;
import org.javarosa.core.services.Logger;
import org.javarosa.core.services.storage.StorageFullException;
import org.kxml2.io.KXmlParser;
import org.xmlpull.v1.XmlPullParserException;

import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.util.Pair;

/**
 * @author ctsims
 */
public abstract class FormRecordCleanupTask<R> extends CommCareTask<Void, Integer, Integer,R> {
    private final Context context;
    private final CommCarePlatform platform;

    public static final int STATUS_CLEANUP = -1;
    private static final int SUCCESS = -1;

    public FormRecordCleanupTask(Context context, CommCarePlatform platform, int taskId) {
        this.context = context;
        this.platform = platform;
        this.taskId = taskId;
    }

    /*
     * (non-Javadoc)
     * @see org.commcare.android.tasks.templates.CommCareTask#doTaskBackground(java.lang.Object[])
     */
    @Override
    protected Integer doTaskBackground(Void... params) {
        SqlStorage<FormRecord> storage = CommCareApplication._().getUserStorage(FormRecord.class);

        Vector<Integer> recordsToRemove = storage.getIDsForValues(new String[] { FormRecord.META_STATUS}, new String[] { FormRecord.STATUS_SAVED });
        int oldrecords = recordsToRemove.size();

        Vector<Integer> unindexedRecords = storage.getIDsForValues(new String[] { FormRecord.META_STATUS}, new String[] { FormRecord.STATUS_UNINDEXED });
        int count = 0;
        for(int recordID : unindexedRecords) {
            FormRecord r = storage.read(recordID);

            try {
                updateAndWriteRecord(context, platform, r,
                        FormRecord.STATUS_SAVED, storage);
            } catch (FileNotFoundException | InvalidStructureException e) {
                // No form or bad form data, mark for deletion
                recordsToRemove.add(recordID);
            } catch(XmlPullParserException | IOException |
                    UnfullfilledRequirementsException e) {
                // Not really sure what happened; just skip
            }

            count++;
            this.publishProgress(count, unindexedRecords.size());
        }

        this.publishProgress(STATUS_CLEANUP);

        SqlStorage<SessionStateDescriptor> ssdStorage =
            CommCareApplication._().getUserStorage(SessionStateDescriptor.class);

        for(int recordID : recordsToRemove) {
            //We don't know anything about the session yet, so give it -1 to flag that
            wipeRecord(context, -1, recordID, storage, ssdStorage);
        }

        System.out.println("Synced: " + unindexedRecords.size() +
                ". Removed: " + oldrecords + " old records, and " +
                (recordsToRemove.size() - oldrecords) + " busted new ones");

        return SUCCESS;
    }

    /**
     * Reparse the saved form instance associated with the form record and
     * apply any updates found to the form record, such as UUID and date
     * modified, returning an updated copy.  Write the updated record to
     * storage. If the record is associated with a case id, recompute and write
     * the SessionStateDescriptor too.
     *
     * @param context   Used to get the filepath of the form instance
     *                  associated with the record.
     * @param oldRecord Reparse this record and return an updated copy of it
     * @param newStatus The new form record status
     * @param storage   User storage where updated FormRecord is written
     * @return The reparsed form record and the associated case id, if present
     * @throws IOException                       Problem opening the saved form
     *                                           attached to the record.
     * @throws InvalidStructureException         Occurs during reparsing of the
     *                                           form attached to record.
     * @throws XmlPullParserException
     * @throws UnfullfilledRequirementsException Parsing encountered a platform
     *                                           versioning problem
     */
    public static FormRecord updateAndWriteRecord(Context context,
                                                  CommCarePlatform platform,
                                                  FormRecord oldRecord,
                                                  String newStatus,
                                                  SqlStorage<FormRecord> storage)
            throws InvalidStructureException, IOException,
            XmlPullParserException, UnfullfilledRequirementsException {

        Pair<FormRecord, String> recordUpdates =
            reparseRecord(context, oldRecord, FormRecord.STATUS_SAVED);
        FormRecord updated = recordUpdates.first;
        String caseId = recordUpdates.second;

        if (caseId != null &&
                FormRecord.STATUS_UNINDEXED.equals(oldRecord.getStatus()) &&
                !FormRecord.STATUS_UNINDEXED.equals(newStatus)) {
            AndroidSessionWrapper asw
                = AndroidSessionWrapper.mockEasiestRoute(platform,
                        oldRecord.getFormNamespace(), caseId);
            asw.setFormRecordId(updated.getID());

            SqlStorage<SessionStateDescriptor> ssdStorage =
                CommCareApplication._().getUserStorage(SessionStateDescriptor.class);

            try {
                ssdStorage.write(asw.getSessionStateDescriptor());
            } catch (StorageFullException e) {
                // TODO PLM should we abort completely here?
            }
        }

        storage.write(updated);
        return updated;
    }

    /**
     * Reparse the saved form instance associated with the form record and
     * apply any updates found to the form record, such as UUID and date
     * modified, returning an updated copy.
     *
     * @param context   Used to get the filepath of the form instance
     *                  associated with the record.
     * @param r         Reparse this record and return an updated copy of it
     * @param newStatus The new form record status
     * @return The reparsed form record and the associated case id, if present
     * @throws IOException                       Problem opening the saved form
     *                                           attached to the record.
     * @throws InvalidStructureException         Occurs during reparsing of the
     *                                           form attached to record.
     * @throws XmlPullParserException
     * @throws UnfullfilledRequirementsException Parsing encountered a platform
     *                                           versioning problem
     */
    private static Pair<FormRecord, String> reparseRecord(Context context,
                                                         FormRecord r,
                                                         String newStatus)
            throws IOException, InvalidStructureException,
            XmlPullParserException, UnfullfilledRequirementsException {
        final String[] caseIDs = new String[1];
        final Date[] modified = new Date[]{new Date(0)};
        final String[] uuid = new String[1];

        // NOTE: This does _not_ parse and process the case data. It's only for
        // getting meta information about the entry session.
        TransactionParserFactory factory = new TransactionParserFactory() {
            @Override
            public TransactionParser getParser(String name, String namespace, KXmlParser parser) {
                if ("case".equals(name)) {
                    return buildCaseParser(namespace, parser, caseIDs);
                } else if ("meta".equalsIgnoreCase(name)) {
                    return buildMetaParser(uuid, modified, parser);
                }
                return null;
            }
        };

        String path = r.getPath(context);
        InputStream is;
        FileInputStream fis = new FileInputStream(path);
        try {
            Cipher decrypter = Cipher.getInstance("AES");
            decrypter.init(Cipher.DECRYPT_MODE, new SecretKeySpec(r.getAesKey(), "AES"));
            is = new CipherInputStream(fis, decrypter);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            throw new RuntimeException("No Algorithm while attempting to decode form submission for processing");
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
            throw new RuntimeException("Invalid cipher data while attempting to decode form submission for processing");
        } catch (InvalidKeyException e) {
            e.printStackTrace();
            throw new RuntimeException("Invalid Key Data while attempting to decode form submission for processing");
        } finally {
            fis.close();
        }

        //Construct parser for this form's internal data.
        DataModelPullParser parser = new DataModelPullParser(is, factory);
        parser.parse();

        // TODO: We should be committing all changes to form record models via
        // the ASW objects, not manually.
        FormRecord parsed = new FormRecord(r.getInstanceURI().toString(),
                newStatus, r.getFormNamespace(), r.getAesKey(),
                uuid[0], modified[0]);
        parsed.setID(r.getID());

        // Make sure that the instance is no longer editable
        if (!newStatus.equals(FormRecord.STATUS_INCOMPLETE) &&
                !newStatus.equals(FormRecord.STATUS_UNSTARTED)) {
            ContentValues cv = new ContentValues();
            cv.put(InstanceColumns.CAN_EDIT_WHEN_COMPLETE, Boolean.toString(false));
            context.getContentResolver().update(r.getInstanceURI(), cv, null, null);
        }

        return new Pair<>(parsed, caseIDs[0]);
    }

    public static void wipeRecord(Context c,SessionStateDescriptor existing) {
        int ssid = existing.getID();
        int formRecordId = existing.getFormRecordId();
        wipeRecord(c, ssid, formRecordId);
    }

    public static void wipeRecord(Context c, AndroidSessionWrapper currentState) {
        int formRecordId = currentState.getFormRecordId();
        int ssdId = currentState.getSessionDescriptorId();
        wipeRecord(c, ssdId, formRecordId);
    }

    public static void wipeRecord(Context c, FormRecord record) {
        wipeRecord(c, -1, record.getID());
    }

    public static void wipeRecord(Context c, int formRecordId) {
        wipeRecord(c, -1, formRecordId);
    }

    public static void wipeRecord(Context c, int sessionId, int formRecordId) {
        wipeRecord(c, sessionId, formRecordId,
                CommCareApplication._().getUserStorage(FormRecord.class),
                CommCareApplication._().getUserStorage(SessionStateDescriptor.class));
    }

    private static void wipeRecord(Context context, int sessionId,
                                   int formRecordId,
                                   SqlStorage<FormRecord> frStorage,
                                   SqlStorage<SessionStateDescriptor> ssdStorage) {
        if(sessionId != -1) {
            try {
                SessionStateDescriptor ssd = ssdStorage.read(sessionId);

                int ssdFrid = ssd.getFormRecordId();
                if(formRecordId == -1) {
                    formRecordId = ssdFrid;
                } else if(formRecordId != ssdFrid) {
                    //Not good.
                    Logger.log(AndroidLogger.TYPE_ERROR_ASSERTION,
                            "Inconsistent formRecordId's in session storage");
                }
            } catch(Exception e) {
                Logger.log(AndroidLogger.TYPE_ERROR_ASSERTION,
                        "Session ID exists, but with no record (or broken record)");
            }
        }
        String dataPath = null;

        if(formRecordId != -1 ) {
            try {
                FormRecord r = frStorage.read(formRecordId);
                dataPath = r.getPath(context);

                //See if there is a hanging session ID for this
                if(sessionId == -1) {
                    Vector<Integer> sessionIds = ssdStorage.getIDsForValue(SessionStateDescriptor.META_FORM_RECORD_ID, formRecordId);
                    // We really shouldn't be able to end up with sessionId's
                    // that point to more than one thing.
                    if(sessionIds.size() == 1) {
                        sessionId = sessionIds.firstElement();
                    } else if(sessionIds.size() > 1) {
                        sessionId = sessionIds.firstElement();
                        Logger.log(AndroidLogger.TYPE_ERROR_ASSERTION,
                                "Multiple session ID's pointing to the same form record");
                    }
                }
            } catch(Exception e) {
                Logger.log(AndroidLogger.TYPE_ERROR_ASSERTION,
                        "Session ID exists, but with no record (or broken record)");
            }
        }

        //Delete 'em if you got 'em
        if(sessionId != -1) {
            ssdStorage.remove(sessionId);
        }
        if(formRecordId != -1) {
            frStorage.remove(formRecordId);
        }

        if(dataPath != null) {
            String selection = InstanceColumns.INSTANCE_FILE_PATH +"=?";
            Cursor c = context.getContentResolver().query(InstanceColumns.CONTENT_URI, new String[] {InstanceColumns._ID}, selection, new String[] {dataPath}, null);
            if(c.moveToFirst()) {
                //There's a cursor for this file, good.
                long id = c.getLong(0);

                //this should take care of the files
                context.getContentResolver().delete(ContentUris.withAppendedId(InstanceColumns.CONTENT_URI, id), null, null);
            } else{
                //No instance record for whatever reason, manually wipe files
                FileUtil.deleteFileOrDir(dataPath);
            }
            c.close();
        }
    }

    private static TransactionParser buildCaseParser(String namespace, KXmlParser parser, final String[] caseIDs) {
        //If we have a proper 2.0 namespace, good.
        if (CaseXmlParser.CASE_XML_NAMESPACE.equals(namespace)) {
            return new AndroidCaseXmlParser(parser, CommCareApplication._().getUserStorage(ACase.STORAGE_KEY, ACase.class)) {

                /*
                 * (non-Javadoc)
                 * @see org.commcare.xml.CaseXmlParser#commit(org.commcare.cases.model.Case)
                 */
                @Override
                public void commit(Case parsed) throws IOException, SessionUnavailableException {
                    String incoming = parsed.getCaseId();
                    if (incoming != null && !"".equals(incoming)) {
                        caseIDs[0] = incoming;
                    }
                }

                /*
                 * (non-Javadoc)
                 * @see org.commcare.xml.CaseXmlParser#retrieve(java.lang.String)
                 */
                @Override
                public ACase retrieve(String entityId) throws SessionUnavailableException {
                    caseIDs[0] = entityId;
                    ACase c = new ACase("", "");
                    c.setCaseId(entityId);
                    return c;
                }
            };
        } else {
            // Otherwise, this gets more tricky. Ideally we'd want to
            // skip this block for compatibility purposes, but we can
            // at least try to get a caseID (which is all we want)
            return new BestEffortBlockParser(parser, null, null, new String[]{"case_id"}) {
                /*
                 * (non-Javadoc)
                 * @see org.commcare.xml.BestEffortBlockParser#commit(java.util.Hashtable)
                 */
                @Override
                public void commit(Hashtable<String, String> values) {
                    if (values.containsKey("case_id")) {
                        caseIDs[0] = values.get("case_id");
                    }
                }
            };
        }
    }
    private static TransactionParser buildMetaParser(final String[] uuid, final Date[] modified, KXmlParser parser) {
        return new MetaDataXmlParser(parser) {
            /*
             * (non-Javadoc)
             * @see org.commcare.xml.MetaDataXmlParser#commit(java.lang.String[])
             */
            @Override
            public void commit(String[] meta) throws IOException, SessionUnavailableException {
                if (meta[0] != null) {
                    modified[0] = DateUtils.parseDateTime(meta[0]);
                }
                uuid[0] = meta[1];
            }
        };
    }


}
<commit_msg>Comment<commit_after>package org.commcare.android.tasks;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.Hashtable;
import java.util.Vector;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;

import org.commcare.android.database.SqlStorage;
import org.commcare.android.database.user.models.ACase;
import org.commcare.android.database.user.models.FormRecord;
import org.commcare.android.database.user.models.SessionStateDescriptor;
import org.commcare.android.javarosa.AndroidLogger;
import org.commcare.android.models.AndroidSessionWrapper;
import org.commcare.android.tasks.templates.CommCareTask;
import org.commcare.android.util.FileUtil;
import org.commcare.android.util.SessionUnavailableException;
import org.commcare.cases.model.Case;
import org.commcare.dalvik.application.CommCareApplication;
import org.commcare.dalvik.odk.provider.InstanceProviderAPI.InstanceColumns;
import org.commcare.data.xml.DataModelPullParser;
import org.commcare.data.xml.TransactionParser;
import org.commcare.data.xml.TransactionParserFactory;
import org.commcare.util.CommCarePlatform;
import org.commcare.xml.AndroidCaseXmlParser;
import org.commcare.xml.BestEffortBlockParser;
import org.commcare.xml.CaseXmlParser;
import org.commcare.xml.MetaDataXmlParser;
import org.javarosa.xml.util.InvalidStructureException;
import org.javarosa.xml.util.UnfullfilledRequirementsException;
import org.javarosa.core.model.utils.DateUtils;
import org.javarosa.core.services.Logger;
import org.javarosa.core.services.storage.StorageFullException;
import org.kxml2.io.KXmlParser;
import org.xmlpull.v1.XmlPullParserException;

import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.util.Pair;

/**
 * @author ctsims
 */
public abstract class FormRecordCleanupTask<R> extends CommCareTask<Void, Integer, Integer,R> {
    private final Context context;
    private final CommCarePlatform platform;

    public static final int STATUS_CLEANUP = -1;
    private static final int SUCCESS = -1;

    public FormRecordCleanupTask(Context context, CommCarePlatform platform, int taskId) {
        this.context = context;
        this.platform = platform;
        this.taskId = taskId;
    }

    /*
     * (non-Javadoc)
     * @see org.commcare.android.tasks.templates.CommCareTask#doTaskBackground(java.lang.Object[])
     */
    @Override
    protected Integer doTaskBackground(Void... params) {
        SqlStorage<FormRecord> storage = CommCareApplication._().getUserStorage(FormRecord.class);

        Vector<Integer> recordsToRemove = storage.getIDsForValues(new String[] { FormRecord.META_STATUS}, new String[] { FormRecord.STATUS_SAVED });
        int oldrecords = recordsToRemove.size();

        Vector<Integer> unindexedRecords = storage.getIDsForValues(new String[] { FormRecord.META_STATUS}, new String[] { FormRecord.STATUS_UNINDEXED });
        int count = 0;
        for(int recordID : unindexedRecords) {
            FormRecord r = storage.read(recordID);

            try {
                updateAndWriteRecord(context, platform, r,
                        FormRecord.STATUS_SAVED, storage);
            } catch (FileNotFoundException | InvalidStructureException e) {
                // No form or bad form data, mark for deletion
                recordsToRemove.add(recordID);
            } catch(XmlPullParserException | IOException |
                    UnfullfilledRequirementsException e) {
                // Not really sure what happened; just skip
            }

            count++;
            this.publishProgress(count, unindexedRecords.size());
        }

        this.publishProgress(STATUS_CLEANUP);

        SqlStorage<SessionStateDescriptor> ssdStorage =
            CommCareApplication._().getUserStorage(SessionStateDescriptor.class);

        for(int recordID : recordsToRemove) {
            //We don't know anything about the session yet, so give it -1 to flag that
            wipeRecord(context, -1, recordID, storage, ssdStorage);
        }

        System.out.println("Synced: " + unindexedRecords.size() +
                ". Removed: " + oldrecords + " old records, and " +
                (recordsToRemove.size() - oldrecords) + " busted new ones");

        return SUCCESS;
    }

    /**
     * Reparse the saved form instance associated with the form record and
     * apply any updates found to the form record, such as UUID and date
     * modified, returning an updated copy.  Write the updated record to
     * storage. If the record is associated with a case id, recompute and write
     * the SessionStateDescriptor too.
     *
     * @param context   Used to get the filepath of the form instance
     *                  associated with the record.
     * @param oldRecord Reparse this record and return an updated copy of it
     * @param newStatus The new form record status
     * @param storage   User storage where updated FormRecord is written
     * @return The reparsed form record and the associated case id, if present
     * @throws IOException                       Problem opening the saved form
     *                                           attached to the record.
     * @throws InvalidStructureException         Occurs during reparsing of the
     *                                           form attached to record.
     * @throws XmlPullParserException
     * @throws UnfullfilledRequirementsException Parsing encountered a platform
     *                                           versioning problem
     */
    public static FormRecord updateAndWriteRecord(Context context,
                                                  CommCarePlatform platform,
                                                  FormRecord oldRecord,
                                                  String newStatus,
                                                  SqlStorage<FormRecord> storage)
            throws InvalidStructureException, IOException,
            XmlPullParserException, UnfullfilledRequirementsException {

        Pair<FormRecord, String> recordUpdates =
            reparseRecord(context, oldRecord, FormRecord.STATUS_SAVED);
        FormRecord updated = recordUpdates.first;
        String caseId = recordUpdates.second;

        if (caseId != null &&
                FormRecord.STATUS_UNINDEXED.equals(oldRecord.getStatus()) &&
                !FormRecord.STATUS_UNINDEXED.equals(newStatus)) {
            // There is a case id associated with an unidexed form record,
            // calculate the state descripter and write it.
            AndroidSessionWrapper asw
                = AndroidSessionWrapper.mockEasiestRoute(platform,
                        oldRecord.getFormNamespace(), caseId);
            asw.setFormRecordId(updated.getID());

            SqlStorage<SessionStateDescriptor> ssdStorage =
                CommCareApplication._().getUserStorage(SessionStateDescriptor.class);

            try {
                ssdStorage.write(asw.getSessionStateDescriptor());
            } catch (StorageFullException e) {
                // TODO PLM should we abort completely here?
            }
        }

        storage.write(updated);
        return updated;
    }

    /**
     * Reparse the saved form instance associated with the form record and
     * apply any updates found to the form record, such as UUID and date
     * modified, returning an updated copy.
     *
     * @param context   Used to get the filepath of the form instance
     *                  associated with the record.
     * @param r         Reparse this record and return an updated copy of it
     * @param newStatus The new form record status
     * @return The reparsed form record and the associated case id, if present
     * @throws IOException                       Problem opening the saved form
     *                                           attached to the record.
     * @throws InvalidStructureException         Occurs during reparsing of the
     *                                           form attached to record.
     * @throws XmlPullParserException
     * @throws UnfullfilledRequirementsException Parsing encountered a platform
     *                                           versioning problem
     */
    private static Pair<FormRecord, String> reparseRecord(Context context,
                                                         FormRecord r,
                                                         String newStatus)
            throws IOException, InvalidStructureException,
            XmlPullParserException, UnfullfilledRequirementsException {
        final String[] caseIDs = new String[1];
        final Date[] modified = new Date[]{new Date(0)};
        final String[] uuid = new String[1];

        // NOTE: This does _not_ parse and process the case data. It's only for
        // getting meta information about the entry session.
        TransactionParserFactory factory = new TransactionParserFactory() {
            @Override
            public TransactionParser getParser(String name, String namespace, KXmlParser parser) {
                if ("case".equals(name)) {
                    return buildCaseParser(namespace, parser, caseIDs);
                } else if ("meta".equalsIgnoreCase(name)) {
                    return buildMetaParser(uuid, modified, parser);
                }
                return null;
            }
        };

        String path = r.getPath(context);
        InputStream is;
        FileInputStream fis = new FileInputStream(path);
        try {
            Cipher decrypter = Cipher.getInstance("AES");
            decrypter.init(Cipher.DECRYPT_MODE, new SecretKeySpec(r.getAesKey(), "AES"));
            is = new CipherInputStream(fis, decrypter);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            throw new RuntimeException("No Algorithm while attempting to decode form submission for processing");
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
            throw new RuntimeException("Invalid cipher data while attempting to decode form submission for processing");
        } catch (InvalidKeyException e) {
            e.printStackTrace();
            throw new RuntimeException("Invalid Key Data while attempting to decode form submission for processing");
        } finally {
            fis.close();
        }

        //Construct parser for this form's internal data.
        DataModelPullParser parser = new DataModelPullParser(is, factory);
        parser.parse();

        // TODO: We should be committing all changes to form record models via
        // the ASW objects, not manually.
        FormRecord parsed = new FormRecord(r.getInstanceURI().toString(),
                newStatus, r.getFormNamespace(), r.getAesKey(),
                uuid[0], modified[0]);
        parsed.setID(r.getID());

        // Make sure that the instance is no longer editable
        if (!newStatus.equals(FormRecord.STATUS_INCOMPLETE) &&
                !newStatus.equals(FormRecord.STATUS_UNSTARTED)) {
            ContentValues cv = new ContentValues();
            cv.put(InstanceColumns.CAN_EDIT_WHEN_COMPLETE, Boolean.toString(false));
            context.getContentResolver().update(r.getInstanceURI(), cv, null, null);
        }

        return new Pair<>(parsed, caseIDs[0]);
    }

    public static void wipeRecord(Context c,SessionStateDescriptor existing) {
        int ssid = existing.getID();
        int formRecordId = existing.getFormRecordId();
        wipeRecord(c, ssid, formRecordId);
    }

    public static void wipeRecord(Context c, AndroidSessionWrapper currentState) {
        int formRecordId = currentState.getFormRecordId();
        int ssdId = currentState.getSessionDescriptorId();
        wipeRecord(c, ssdId, formRecordId);
    }

    public static void wipeRecord(Context c, FormRecord record) {
        wipeRecord(c, -1, record.getID());
    }

    public static void wipeRecord(Context c, int formRecordId) {
        wipeRecord(c, -1, formRecordId);
    }

    public static void wipeRecord(Context c, int sessionId, int formRecordId) {
        wipeRecord(c, sessionId, formRecordId,
                CommCareApplication._().getUserStorage(FormRecord.class),
                CommCareApplication._().getUserStorage(SessionStateDescriptor.class));
    }

    private static void wipeRecord(Context context, int sessionId,
                                   int formRecordId,
                                   SqlStorage<FormRecord> frStorage,
                                   SqlStorage<SessionStateDescriptor> ssdStorage) {
        if(sessionId != -1) {
            try {
                SessionStateDescriptor ssd = ssdStorage.read(sessionId);

                int ssdFrid = ssd.getFormRecordId();
                if(formRecordId == -1) {
                    formRecordId = ssdFrid;
                } else if(formRecordId != ssdFrid) {
                    //Not good.
                    Logger.log(AndroidLogger.TYPE_ERROR_ASSERTION,
                            "Inconsistent formRecordId's in session storage");
                }
            } catch(Exception e) {
                Logger.log(AndroidLogger.TYPE_ERROR_ASSERTION,
                        "Session ID exists, but with no record (or broken record)");
            }
        }
        String dataPath = null;

        if(formRecordId != -1 ) {
            try {
                FormRecord r = frStorage.read(formRecordId);
                dataPath = r.getPath(context);

                //See if there is a hanging session ID for this
                if(sessionId == -1) {
                    Vector<Integer> sessionIds = ssdStorage.getIDsForValue(SessionStateDescriptor.META_FORM_RECORD_ID, formRecordId);
                    // We really shouldn't be able to end up with sessionId's
                    // that point to more than one thing.
                    if(sessionIds.size() == 1) {
                        sessionId = sessionIds.firstElement();
                    } else if(sessionIds.size() > 1) {
                        sessionId = sessionIds.firstElement();
                        Logger.log(AndroidLogger.TYPE_ERROR_ASSERTION,
                                "Multiple session ID's pointing to the same form record");
                    }
                }
            } catch(Exception e) {
                Logger.log(AndroidLogger.TYPE_ERROR_ASSERTION,
                        "Session ID exists, but with no record (or broken record)");
            }
        }

        //Delete 'em if you got 'em
        if(sessionId != -1) {
            ssdStorage.remove(sessionId);
        }
        if(formRecordId != -1) {
            frStorage.remove(formRecordId);
        }

        if(dataPath != null) {
            String selection = InstanceColumns.INSTANCE_FILE_PATH +"=?";
            Cursor c = context.getContentResolver().query(InstanceColumns.CONTENT_URI, new String[] {InstanceColumns._ID}, selection, new String[] {dataPath}, null);
            if(c.moveToFirst()) {
                //There's a cursor for this file, good.
                long id = c.getLong(0);

                //this should take care of the files
                context.getContentResolver().delete(ContentUris.withAppendedId(InstanceColumns.CONTENT_URI, id), null, null);
            } else{
                //No instance record for whatever reason, manually wipe files
                FileUtil.deleteFileOrDir(dataPath);
            }
            c.close();
        }
    }

    private static TransactionParser buildCaseParser(String namespace, KXmlParser parser, final String[] caseIDs) {
        //If we have a proper 2.0 namespace, good.
        if (CaseXmlParser.CASE_XML_NAMESPACE.equals(namespace)) {
            return new AndroidCaseXmlParser(parser, CommCareApplication._().getUserStorage(ACase.STORAGE_KEY, ACase.class)) {

                /*
                 * (non-Javadoc)
                 * @see org.commcare.xml.CaseXmlParser#commit(org.commcare.cases.model.Case)
                 */
                @Override
                public void commit(Case parsed) throws IOException, SessionUnavailableException {
                    String incoming = parsed.getCaseId();
                    if (incoming != null && !"".equals(incoming)) {
                        caseIDs[0] = incoming;
                    }
                }

                /*
                 * (non-Javadoc)
                 * @see org.commcare.xml.CaseXmlParser#retrieve(java.lang.String)
                 */
                @Override
                public ACase retrieve(String entityId) throws SessionUnavailableException {
                    caseIDs[0] = entityId;
                    ACase c = new ACase("", "");
                    c.setCaseId(entityId);
                    return c;
                }
            };
        } else {
            // Otherwise, this gets more tricky. Ideally we'd want to
            // skip this block for compatibility purposes, but we can
            // at least try to get a caseID (which is all we want)
            return new BestEffortBlockParser(parser, null, null, new String[]{"case_id"}) {
                /*
                 * (non-Javadoc)
                 * @see org.commcare.xml.BestEffortBlockParser#commit(java.util.Hashtable)
                 */
                @Override
                public void commit(Hashtable<String, String> values) {
                    if (values.containsKey("case_id")) {
                        caseIDs[0] = values.get("case_id");
                    }
                }
            };
        }
    }
    private static TransactionParser buildMetaParser(final String[] uuid, final Date[] modified, KXmlParser parser) {
        return new MetaDataXmlParser(parser) {
            /*
             * (non-Javadoc)
             * @see org.commcare.xml.MetaDataXmlParser#commit(java.lang.String[])
             */
            @Override
            public void commit(String[] meta) throws IOException, SessionUnavailableException {
                if (meta[0] != null) {
                    modified[0] = DateUtils.parseDateTime(meta[0]);
                }
                uuid[0] = meta[1];
            }
        };
    }


}

{'fraction_non_alphanumeric': 0.08077766331193703, 'fraction_numerical': 0.001796233193332805, 'mean_word_length': 1.868030303030303, 'pattern_counts': {'":': 0, '<': 33, '<?xml version=': 0, '>': 35, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 30}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 54, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '935712', 'n_tokens_mistral': 9471, 'n_tokens_neox': 8948, 'n_words': 2358}